본문 바로가기
FE/JavaScript

[JavaScript] this

by thedev 2023. 3. 26.

 

this

 


 

# this란

 

 this는 값을 가르키는 키워드이다. this가 가르키는 값은 실행 컨텍스트가 생성될 때 함께 결정된다. 만약 함수를 호출한다면, 함수가 호출될 때 실행 컨텍스트가 생성되고 this가 가르키는 값도 결정된다. 다양한 상황에서 this가 어떤 값을 가르키게 되는지 알아보자.

 

 

# 전역 객체를 가르키는 this

 

 전역 공간에서 this는 전역 객체를 가르킨다. 여기서 전역 객체란, 브라우저에서는 window, Node.js에서는 global이 된다. 전역 객체에는 특이한 성질이 있다. 전역 공간에 변수를 선언하면, 자바스크립트 엔진은 그 변수를 전역 객체의 프로퍼티로도 할당한다. 즉, 전역 공간에서 선언된 변수는 변수이자 전역 객체의 프로퍼티가 된다.

 

var a = 1;

console.log(a); // 1
console.log(window.a); // 1
console.log(this.a); // 1

 

 따라서 a, window.a, this.a는 모두 같은 값을 가진다. 전역 공간에서 선언한 a는 전역 객체 window의 프로퍼티도 되기 때문에 window.a로도 표현 가능하고, this는 전역 객체를 가르키기 때문에 this.a 또한 1이라는 값이 출력될 수 있다. 그러나 이는 var로 선언한 변수에만 해당한다. ES6부터 등장한 let, const는 전역객체 프로퍼티가 되지 않는다.

 

window.b = 2;
console.log(b, window.b, this.b); // 2, 2, 2

 

 그래서 window.b로 값을 할당하는 것도 가능하다. window로 값을 할당해도 var로 선언한 것과 유사하게 동작한다.

 

 

# 메소드 vs 함수

 

 this의 문제는 함수에서 발생한다. 우선 함수와 메소드의 차이를 짚고 가자. 간단히 설명하면 메소드는 객체의 프로퍼티로서 할당된 함수라고도 볼 수 있다. 함수는 독립적으로 그 기능을 수행하지만, 메소드는 자신이 할당되어 있는 객체에 관해서만 동작할 수 있다.

 

const example = function(x) {
   console.log(this, x);
};

example(1); // Window {...} 1

const obj = {
   inner: example
};

obj.inner(2); // inner {...} 2

 

 함수를 만들어서 하나는 전역에서 호출하고, 다른 하나는 객체 안에 프로퍼티로 넣어서 호출하였다. 이때, 각각의 this는 서로 다른 것을 가르킨다. 전역에서 호출된 함수는 this가 Window 객체를 가르키고 있고 객체 내에서 호출된, 즉 메소드로서 호출된 함수의 this는 자신이 속한 곳의 객체 obj를 가르킨다.

 

 

1. 메소드 내부에서의 this

 

 함수를 객체 안에서, 즉 메소드로서 호출하는 경우에 this는 호출한 주체를 가르킨다. 마지막 점 앞에 명시된 객체가 this가 가르키는 대상이 된다.

 

  const obj = {
    method1: function (x) {
      console.log(this);
    },
    inner: {
      method2: function () {
        console.log(this);
      },
    },
  };

obj.method1(); // {inner: {…}, method1: ƒ}
obj.inner.method2(); // {method2: ƒ}

 

 

2. 함수 내부에서의 this

 

 어떤 함수를 메소드가 아닌, 그냥 함수로서 호출할 경우에는 this가 따로 지정되지 않는다. 따라서 함수에서의 this는 전역 객체를 가르키게 된다. 그렇다면 함수와 메소드가 섞여있는 다음 예시를 알아보자. (1), (2), (3)의 this는 각각 어떤 객체를 가르키고 있을까?

 

  const obj = {
    outer: function () {
      console.log(this); // (1)
      const innerFunction = function () {
        console.log(this);
      };
      innerFunction(); // (2)
      const obj2 = {
        innerMethod: innerFunction,
      };
      obj2.innerMethod(); // (3)
    },
  };
  obj.outer();

 

 (1)은 obj, (2)는 전역 객체, (3)은 obj2를 가르킨다. 여기서 이상함이 느껴진다. 분명 (2)는 obj라는 객체 안에서 선언된 함수인데, 정작 this가 가르키는 객체는 obj가 아니라 전역 객체이다. 즉, this는 함수를 실행하는 당시의 주변 환경(메소드 내부인가, 전역인가)에 상관없이 그저 함수를 호출하는 구문 앞에 객체가 있는지 없는지로 판단된다. (2)의 innerFunction()은 메소드 내부에서 실행되었지만 (1), (3)과 다르게 앞에 아무런 객체가 적혀있지 않다. 따라서 그저 전역 객체를 가르키게 된 것이다.

 

 이런 this의 특징 때문에 개발자 입장에서는 this가 가르키는 객체가 무엇인지 파악하기 어려워진다. 이러한 문제를 보완하고자 ES6의 화살표 함수가 도입되었다. 화살표 함수에서는 this가 아무런 객체도 바인딩하지 않는다. 실행 컨텍스트를 생성할 때 this 바인딩 과정 자체가 빠지게 되어, 상위 스코프의 this를 그대로 활용할 수 있도록 해준다.

 

  const obj = {
    outer: function () {
      console.log(this); // (1)
      const innerFunction = () => {
        console.log(this);
      };
      innerFunction(); // (2)
    },
  };
  obj.outer();

 

 이 상황에서 (1)과 (2)의 this는 모두 obj를 가르키게 된다. 화살표 함수에는 this가 없으며, 접근하고자 하면 스코프체인상 가장 가까운 this에 접근한다.

 

 

# 콜백 함수에서의 this

 

 콜백 함수도 기본적으로 함수이기 때문에, 콜백 함수에서의 this도 기본적으로 전역 객체를 가르킨다. 그러나 콜백 함수의 제어권을 가지고 있는 함수에서 별도로 this를 지정하는 경우, 그 대상을 참조하게 된다.

 

// (1)
setTimeout(function () {
  console.log(this);
}, 500);

// (2)
const text = document.getElementById("text");
text.addEventListener("click", function (e) {
  console.log(this);
});

 

 (1)에서 this는 전역 객체를 가르키지만, (2)에서의 this는 클릭 이벤트와 관련된 객체를 가르킨다. (1)에서는 this가 가르킬 대상이 따로 지정되지 않았지만 (2)에서는 콜백 함수를 호출할 때 addEventListener 메소드가 자신의 this를 상속하도록 정의가 되었기 때문이다. 따라서 메소드의 점(.) 앞부분인 text가 this가 가르키는 객체가 된다. 따라서 콜백 함수에서는 this를 정확하게 정의할 수 없고 상황마다 달라진다.

 

 

# class 함수에서의 this

 

 클래스 함수에서 this는 각각의 변수를 가르킨다.

 

class UserList {
  constructor(name, age, id) {
    this.name = name;
    this.age = age;
    this.id = id;
  }
}

const user1 = new UserList("kim", 20, 123);
const user2 = new UserList("lee", 21, 345);

 

 

const user1 = new UserList("kim", 20, 123); 내부에서의 this는 user1을 가르키고

const user2 = new UserList("lee", 21, 345); 내부에서의 this는 user2를 가르킨다.

 


 

# 명시적으로 this 바인딩하기

 

 앞에서는 상황에 따라 this가 어디에 바인딩되는지 살펴보았다. 그렇다면 this에 직접 값을 바인딩하는 방법도 알아보자.

 

 

1. call 메소드 이용하기

 

const obj = {
  a: 1,
  method: function (x, y) {
    console.log(this.a, x, y);
  },
};

obj.method(2, 3); // 1 2 3
obj.method.call({ a: 4 }, 5, 6); // 4 5 6

 

 call 메소드는 메소드의 호출 주체 함수를 즉시 실행하도록 하는 명령이다. 이때 call 메소드의 첫 번째 인자를 this로 바인딩한다. 함수를 그냥 실행하면 this가 전역 객체를 가르키지만, call 메소드를 이용하면 임의의 객체를 this로 지정할 수 있다.

 

 

2. apply 메소드 이용하기

 

const obj = {
  a: 1,
  method: function (x, y) {
    console.log(this.a, x, y);
  },
};

obj.method(2, 3); // 1 2 3
obj.method.apply({ a: 4 }, [5, 6]); // 4 5 6

 

 apply 메소드는 call 메소드와 기능적으로 완전히 동일하지만 두 번째 인자를 배열로 받는다는 점이 다르다.

 

 

3. bind 메소드 이용하기

 

const func = function (x, y) {
  console.log(this, x, y);
};
func(1, 2); // Window{ ... } 1 2

const bindFunc = func.bind({ a: 1 });
bindFunc(1, 2); // {a: 1} 1 2

 

 bind 메소드도 call 메소드와 거의 비슷하지만 즉시 호출되지 않는다. 넘겨받은 함수를 바탕으로 새로운 this가 바인딩된 함수만 생성한다. 그리고 추가로 호출을 해주어야만 함수가 실행된다.

 

const func = function (x, y) {
  console.log(this, x, y);
};
func(1, 2);

const bindFunc = func.bind({ a: 1 });
bindFunc(1, 2);

console.log(func.name); // func
console.log(bindFunc.name); // bound func

 

 bind 메소드를 이용하여 만들어진 함수는 bound라는 이름이 붙은 name 프로퍼티가 생성된다. call이나 apply보다 코드를 추적하기 쉬워진다는 장점이 있다.

 


 

참고 자료

책 '코어 자바스크립트' 3장