상황에 따라 달라지는 this

2024년 05월 29일 by이호연
thumbnail_3-1.png

자바스크립트의 this

자바스크립트의 this는 다른 언어와는 다릅니다.
Java의 경우를 봅시다.

자바의 this
public Class Person {
  private String name;
 
  public Person(String name) {
    this.name = name;
  }
}

이 코드에서 this는 인스턴스 자신을 가리킵니다.
따라서 this.name는 인스턴스의 멤버 변수 name을 가리킵니다.
그러나 자바스크립트는 함수가 호출되는 방식에 따라 this가 가리키는 대상이 달라집니다. 자바스크립트의 this는 기본적으로 실행 컨텍스트가 생성될 때 결정됩니다.
즉, 함수가 호출될 때 this가 가리키는 대상이 결정됩니다.
다양한 케이스에서의 this를 살펴봅시다.

1. 전역 공간에서의 this

전역 공간에서의 this는 전역 객체를 가리킵니다. 브라우저 환경에서는 window 객체가 전역 객체이며, Node.js 환경에서는 global 객체가 전역 객체입니다.

전역 공간에서의 this(브라우저)
  console.log(this);    // { alert: f, console: f, ... }
  console.log(window);  // { alert: f, console: f, ... }
  console.log(this === window); // true

자바스크립트는 전역변수를 선언하면 이를 전역 객체에 프로퍼티로 추가합니다.
사실, 자바스크립트의 모든 변수는 어떠한 객체의 프로퍼티로 존재합니다.

전역 변수와 전역 객체
var a = 1;
console.log(a); // 1
console.log(window.a); // 1
console.log(this.a); // 1

var 키워드를 이용하여 전역 변수를 선언하더라도 특정 객체의 프로퍼티로 추가됩니다.
여기서 특정 객체는 실행 컨텍스트의 Lexical Environment가 됩니다. 실행 컨텍스트는 변수를 수집해서 Lexical Environment의 프로퍼티로 저장합니다.
이후 어떤 변수를 참조하면, Lexical Environment에서 해당 변수를 찾아 반환합니다.

🤔

실제로는 전역 변수를 선언하면 자바스크립트 엔진은 이를 전역객체의 프로퍼티로 할당한다는 것을 알 수 있습니다.
그렇다면 a를 직접 참조할 때도 1이 나오는 이유는 뭘까요?

대부분의 경우에는 전역 공간에 var로 변수를 선언하는 대신 window의 프로퍼티에 직접 할당하더라도 동일한 결과를 얻을 수 있습니다.

전역 변수와 전역 객체 (2)
var a = 1;
window.b = 2;
console.log(a, window.a, this.a); // 1 1 1
console.log(b, window.b, this.b); // 2 2 2
 
window.a = 3;
b = 4;
console.log(a, window.a, this.a); // 3 3 3
console.log(b, window.b, this.b); // 4 4 4

그러나 '삭제'연산에 대해서는 다르게 동작합니다.

전역 변수와 전역 객체 (3)
var a = 1;
delete window.a; // false
console.log(a, window.a, this.a); // 1 1 1
 
var b = 2;
delete b; // false
console.log(b, window.b, this.b); // 2 2 2
 
window.c = 3;
delete window.c; // true
console.log(c, window.c, this.c); // ReferenceError: c is not defined
 
window.d = 4;
delete d; // true
console.log(d, window.d, this.d); // ReferenceError: d is not defined

처음부터 전역객체의 프로퍼티로 할당한 경우 삭제가 되지만, 전역변수로 선언한 경우 삭제가 되지 않습니다.
전역 변수를 선언하면 자바스크립트 엔진이 자동으로 전역 객체의 프로퍼티로 할당하는데, 이 때 configurable 속성이 false로 설정됩니다.

2. 메서드로서 호출할 때 내부에서의 this

함수 vs 메서드

함수와 메서드의 차이는 호출 방식에 있습니다. 이 둘을 구분하는 차이는 독립성입니다.
함수는 독립적으로 호출되지만, 메서드는 객체에 속해있어 객체의 프로퍼티로 호출됩니다.
자바스크립트는 상황별로 this 키워드에 다른 값을 부여하게 함으로써 이를 구현했습니다.

함수로서 호출, 메서드로서 호출
var func = function (x) {
  console.log(this, x);
};
 
func(1); // Window { ... }, 1
 
var obj = {
  method: func,
};
obj.method(2); // { method: f }, 2

어떤 함수를 객체의 프로퍼티에 할당한다고 해서 메서드가 되는 것이 아닙니다.
객체의 메서드로서 호출할 경우에만 메서드로 동작하고, 그렇지 않으면 함수로 동작합니다.
위의 예제를 보면, 동일한 익명함수를 호출하였지만, 두 경우에서 this가 가리키는 대상이 다릅니다.

메서드 내부에서의 this

this에는 호출한 주체에대한 정보가 담깁니다.
메서드 내부에서 this는 메서드를 호출한 객체를 가리킵니다.

메서드 내부에서의 this
var obj = {
  methodA: function () {
    console.log(this);
  },
  inner: {
    methodB: function () {
      console.log(this);
    },
  },
};
obj.methodA(); // { methodA: f, inner: { methodB: f } }
obj['methodA'](); // { methodA: f, inner: { methodB: f } }
 
obj.inner.methodB(); // { methodB: f }
obj.inner['methodB'](); // { methodB: f }
obj['inner'].methodB(); // { methodB: f }
obj['inner']['methodB'](); // { methodB: f }

this binding

3. 함수로서 호출할 때 내부에서의 this

함수 내부에서의 this

함수를 함수로서 호출할 경우 this가 지정되지 않습니다.
this에는 함수를 호출한 주제가 담긴다고 했습니다. 따라서 함수로서 호출하는 경우에는 호출 주체가 없습니다.
this가 지정되지 않은 경우에 this는 전역 객체를 가리킵니다. 따라서 함수를 함수로서 호출할 경우 this는 전역 객체를 가리킵니다.

더글라스 크락포드(Douglas Crockford)는 이를 명백한 설계상의 오류라고 지적하였습니다.

메서드 내부함수에서의 this

메서드 내부함수에서의 this
var obj1 = {
  outer: function () {
    console.log(this); // (1)
    var innerFunc = function () {
      console.log(this);
    };
    innerFunc(); // (2)
 
    var obj2 = {
      innerMethod: innerFunc,
    };
    obj2.innerMethod(); // (3)
  },
};
obj1.outer();

위 코드에서 (1)의 this는 obj1을 가리킵니다.
(2)의 this는 전역 객체를 가리킵니다.
(3)의 this는 obj2를 가리킵니다.

🤔

메서드 내부함수에서의 this가 전역 객체를 가리키는 이유는 무엇일까요?

메서드 내부함수에서의 this를 우회하는 방법

이렇게 하면 함수 호출 주체에 따른 this의 구분은 명확히 할 수 있습니다.
하지만 직관적으로 함수 호출 주체가 없을 때에는 호출 함수 상위의 this를 상속받았으면 좋을 것 같습니다.
변수를 검색하면 가장 가까운 스코프의 Lexical Environment에서부터 검색하는 스코프 체인처럼 this 역시 현재 컨텍스트에 바인딩된 대상이 없으면 상위 컨텍스트의 this를 참조하는 방법이 있으면 좋을 것 같습니다.
변수를 활용하여 this를 우회할 수 있습니다.

메서드 내부함수에서의 this를 우회하는 방법
var obj = {
  outer: function () {
    console.log(this); // (1) { outer: f }
    var innerFunc = function () {
      console.log(that); //  (2) Window { ... }
    };
    innerFunc();
 
    var self = this;
    var innerFunc2 = function () {
      console.log(self); // (3) { outer: f }
    };
    innerFunc2();
  },
};
obj.outer();

selfthat와 같은 변수를 선언하여 this를 우회할 수 있습니다.
변수가 컨텍스트에 바인딩되는 것을 이용한 방법입니다.

() => { }

ES6에서는 화살표 함수를 도입하였습니다.
화살표 함수는 😇 this 바인딩 과정 자체가 빠지게 되어 😇 상위 스코프의 this를 그대로 활용할 수 있습니다.

화살표 함수를 이용한 this 우회
var obj = {
  outer: function () {
    console.log(this); // (1) { outer: f }
    var innerFunc = () => {
      console.log(this); // (2) { outer: f }
    };
    innerFunc();
  },
};
obj.outer();

그 밖에도 bind, call, apply를 이용하여 this를 지정할 수 있습니다.
이에 대한 내용은 다음 챕터에서 다루겠습니다.

4. 콜백 함수 내부에서의 this

콜백 함수의 구체적인 동작 원리와 정의에 대한 내용은 다음 장에서 다루겠습니다.
여기서는 this 바인딩에 대한 내용만 다루겠습니다.

💡

함수 A의 제어권을 다른 함수 B에게 넘겨주는 것을 콜백 함수라고 합니다.
이때, 함수 A는 함수 B의 내부 로직에 따라 실행되며, this 역시 함수 B 내부 로직에서 정한 규칙에 따라 값이 결정됩니다.

대표적인 콜백 함수의 예시를 확인해봅시다.

콜백 함수 내부에서의 this
setTimeout(function () { console.log(this) }, 300);           // (1)
 
[1, 2, 3, 4, 5].forEach(function (x) {                        // (2)
  console.log(this, x);
});
 
document.body.innerHTML = '<button id="a">클릭</button>';
document.body.querySelector("#a")
  .addEventListener('click', function (e) {                   // (3)
    console.log(this, e);
  });

(1)의 경우, setTimeout 함수의 콜백 함수 내부에서 this는 전역 객체를 가리킵니다.
(2)의 경우, forEach 메서드의 콜백 함수 내부에서 this는 전역 객체를 가리킵니다.
(3)의 경우, addEventListener 메서드의 콜백 함수 내부에서 this는 자신, 즉 addEventListener메서드를 가리킵니다.

이처럼 콜백 함수 내부에서의 this는 호출한 함수에 따라 달라집니다.

5. 생성자 함수 내부에서의 this

생성자 함수는 객체를 생성하는 함수입니다. 객체와 클래스는 7장에서 다루도록 하겠습니다.

자바스크립트에서는 함수에 생성자로서의 역할을 부여했습니다. new 키워드를 사용하여 함수를 생성자 함수로 호출하면 해당 함수가 생성자로서 동작하게 됩니다.
그리고 어떤 함수가 생성자 함수로 호출되면, this는 생성자 함수가 생성할 인스턴스를 가리킵니다.

자바스크립트에서 인스턴스가 만들어지는 과정은 다음과 같습니다.

  1. 생성자 함수 호출
  2. 생성자의 prototype 프로퍼티를 참조하는 __proto__라는 프로퍼티를 가진 빈 객체 생성
  3. 생성자 함수 내부의 this에 빈 객체를 바인딩
  4. 인스턴스 생성 예제로 확인해봅시다.
생성자 함수
var Cat = function (name, age) {
  this.bark = '야옹';
  this.name = name;
  this.age = age;
};
 
var choco = new Cat('초코', 7);
var nabi = new Cat('나비', 5);
console.log(choco, nabi);
 
/** 결과
 * Cat { bark: '야옹', name: '초코', age: 7 }
 * Cat { bark: '야옹', name: '나비', age: 5 }
 */

Cat이라는 변수에 익명 함수를 할당했습니다. 이 함수는 nameage를 인자로 받아 thisbark, name, age를 할당합니다.
new 키워드를 사용하여 Cat 함수를 호출하면 this는 빈 객체를 가리키게 됩니다.
이후 thisbark, name, age를 할당하고, 이를 반환합니다.
이렇게 생성된 인스턴스는 choco, nabi에 할당되어 출력됩니다.

즉 6번 줄에서 실행한 생성자 내부에서의 this는 choco를 가리키고, 7번 줄에서 실행한 생성자 내부에서의 this는 nabi를 가리킵니다.