%EB%B8%94%EB%A1%9C%EA%B7%B8-%EC%8D%B8%EB%84%A4%EC%9D%BC-037


JS의 this는 아직까지도 헷갈린다.

코어 자바스크립트에 정말 자세하고 친절하게 나와있어 이해가 쉬웠지만, 한번 더 정리하며 복기해보자.




this란?

다른 대부분의 객체지향 언어에서 this클래스로 생성한 인스턴스 객체를 의미한다.

하지만 JS에서의 this는 어디서든 사용할 수 있다.

상황에 따라 this가 바라보는 대상(ThisBinding)이 달라진다.



1. 전역 공간에서의 this

전역 공간에서의 this는 전역 객체를 가리킨다.

개념상 전역 컨텍스트를 생성하는 주체가 전역 객체이기 때문이다.

  • window — 브라우저 환경에서의 전역객체
  • global — Node.js 환경에서의 전역객체


아래의 예시를 보면 전역 공간에서 this와 window가 서로 같음을 확인할 수 있다.

// [브라우저 환경]

console.log(this); // {alert: f(), atob: f(), ... }
console.log(window); // {alert: f(), atob: f(), ... }


만약 전역 공간에 변수를 선언하고 window에서 접근하면 어떤 결과가 출력될까.

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

모두 1이 출력되는 것을 확인할 수 있다.

이는 자바스크립트의 모든 변수는 실은 특정 객체의 프로퍼티로서 동작하기 때문이다. 여기서 특정 객체는 LexicalEnvironment이다.

실행 컨텍스트는 특정 변수를 수집해 LexicalEnvironment의 프로퍼티로 저장한다. 이후 어떤 변수를 호출하면 LexicalEnvironment를 조회해서 일치하는 프로퍼티를 찾아 그 값을 반환하게 된다.

전역 컨텍스트의 경우, LexicalEnvironment는 전역 객체를 그대로 참조하게 된다.
(+ 정확하게는 GlobalEnv가 전역 객체를 참조하는데, 전역 컨텍스트의 LexicalEnvironment가 이 GlobalEnv를 참조한다. )


👀실행 컨텍스트, 렉시컬 환경 정리글



하지만 전역 변수 선언과 전역 객체의 프로퍼티 할당은 ‘삭제’ 명령에서 차이점이 있다.

// [전역 변수 선언]
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); // Uncaught ReferenceError

window.d = 4;
delete d; // true
console.log(d, window.d, this.d); // Uncaught ReferenceError

처음부터 전역 객체의 프로퍼티를 할당한 경우에는 삭제가 되지만, 전역변수로 선언한 경우에는 삭제되지 않는다.

전역변수를 선언하면 JS 엔진이 이를 자동으로 전역객체의 프로퍼티로 할당하면서 추가적으로 해당 프로퍼티의 configurable 속성(변경 및 삭제 가능성)을 false로 정의하기 때문이다.



따라서 var로 선언한 전역변수와 전역객체의 프로퍼티는 두 가지 차이가 있다.

  • 호이스팅 여부
  • configurable 여부



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

(+) 함수와 메서드의 차이

함수와 메서드는 미리 정의한 동작을 수행하는 코드 뭉치이다.

이 둘을 구분하는 유일한 차이는 독립성이다.

  • 함수 — 그 자체로 독립적인 기능을 수행
  • 메서드 — 자신을 호출한 대상 객체에 관한 동작을 수행


자바스크립트는 상황별로 this 키워드에 다른 값을 부여함으로써 이 차이를 보여준다.

var func = function (x) {
  console.log(this, x);
};

func(1); // (1) -------  Window { ... } 1            ( === window )

var obj = {
  method: func,
};

obj.method(2); // (2) ------- { method: f } 2         ( === obj )
obj["method"](2); // (2) ------- { method: f } 2      ( === obj )

(1) 익명함수가 할당된 func 변수를 호출했고, this로 전역객체인 Window가 출력된다.

(2) func 함수가 할당된 method 프로퍼티가 있는 객체를 할당한 obj 변수가 있다. 해당 obj 변수의 method를 호출했고, thisobj가 출력된다.


이처럼 원래의 익명함수는 그대로지만, 변수에 담아 호출한 경우와 obj 객체의 프로퍼티에 할당해서 호출한 경우의 this가 다르다.

(1)번의 경우 함수로써 호출된 경우이며, (2)번의 경우 메서드로서 호출된 경우이다.



정리하자면 구분법은 다음과 같다.

  • 메서드로 호출 — 함수 이름(프로퍼티명) 앞에 객체가 명시되어 있는 경우
  • 함수로 호출 — 그렇지 않은 모든 경우


함수와 메서드는 함수 앞에 점 표기법(또는 대괄호 표기법)으로 쉽게 구분할 수 있다.



2-1. 메서드 내부에서의 this

this에는 호출한 주체에 대한 정보가 담긴다.

어떤 함수를 메서드로서 호출할 때, 호출 주체는 함수명(프로퍼티명) 앞의 객체이다.

따라서 위에서 살펴봤듯, 점 표기법(또는 대괄호 표기법) 앞의 객체가 this가 된다.



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

함수를 함수로써 호출할 때는 개발자가 코드에 직접 관여해서 실행하는 것이므로, 호출 주체가 명시되어 있지 않다.

따라서 함수에서의 this는 명시되어 있지 않으므로 전역 객체(window, global)을 가리킨다.

var obj = {
  outer: function () {
    console.log(this); // (1)

    var innerFunc = function () {
      console.log(this); // (2)
    };

    innerFunc();
  },
};

obj.outer();

(1) outer는 메서드로서 호출이므로 obj 객체가 this로 출력된다.

(2) outer 함수(메서드) 내부에서 함수로써 호출된다. 따라서 전역 객체인 Window가 출력된다.


3-1. 메서드 내부의 함수에서 this 우회

메서드 내부 함수에서 this를 구할 때, 전역 객체가 아닌 직전 컨텍스트(상위 스코프)의 this를 바라보게 할 수 있다.


  • 변수 활용

해당 코드처럼 this를 변수에 할당해 사용할 수 있다.

var obj = {
  outer: function () {
    console.log(this); // (1)

    var self = this; // 변수에 this 할당

    var innerFunc = function () {
      console.log(self); // (2)
    };

    innerFunc();
  },
};

obj.outer();

self 변수에 this를 할당하여 내부 함수에서 사용할 수 있다.

따라서 (2)번에서도 (1)번과 동일하게 obj 객체가 출력된다.


  • 화살표 함수(ES6+)

화살표 함수는 this를 바인딩하지 않는다.

정확히, 화살표 함수는 실행 컨텍스트를 생성할 때 this 바인딩 과정 자체가 빠지게 되어, 상위 스코프의 this를 그대로 활용할 수 있다.

var obj = {
  outer: function () {
    console.log(this); // (1)

    var innerFunc = () => {
      // 화살표 함수
      console.log(this); // (2)
    };

    innerFunc();
  },
};

obj.outer();

(1)번과 (2)번 모두 obj 객체가 출력된다.



4. 콜백 함수 호출 시, 함수 내부에서의 this

콜백 함수는 제어권을 다른 함수(또는 메서드)에 넘긴 함수이다.

이때 콜백 함수는 제어권이 넘겨진 다른 함수의 내부 로직에 따라 실행되며, this역시 다른 함수 내부 로직에서 정한 규칙에 따라 값이 제어된다.

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

// (2)
[1, 2, 3].forEach(function (num) {
  console.log(this, num);
});

// (3)
document.body.querySelector("#id").addEventListener("click", function (e) {
  console.log(this, e);
});

(1) setTimeout 함수는 내부에서 콜백 함수를 호출할 때 대상이 될 this를 지정하지 않는다. 따라서 3초 뒤 전역 객체가 출력된다.

(2) forEach 메서드 또한 대상이 될 this를 지정하지 않으므로 1, 2, 3과 함께 전역 객체가 출력된다.

(3) addEventListener 메서드는 콜백 함수를 호출할 때 자신의 this를 상속하도록 정의되어 있다. 따라서 document.body.querySelector('#id')의 대상이 this로 출력된다.



정리하자면 다음과 같다.

  • 콜백함수의 제어권을 가지는 함수(메서드)가 콜백 함수에서의 this를 결정한다.
  • 특별히 정의되지 않은 경우, 일반 함수와 동일하게 전역 객체를 바라본다.



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

프로그래밍적으로 ‘생성자’는 구체적인 인스턴스를 만들기 위한 일종의 틀이다.

자바스크립트에서는 new 명령어와 함께 함수를 호출하면 해당 함수가 생성자로서 동작하게 된다.

var Cat = function (name, age) {
  this.name = name;
  this.age = age;
};

var nana = new Cat("나나", 5);
var choco = new Cat("초코", 8);

console.log(nana, choco);
/*  Cat { name: '나나', age : 5 }
    Cat { name: '초코', age : 8 }  */

위 코드에서는 Cat이라는 변수에 익명 함수를 할당했다.

해당 익명 함수 내부에서는 this에 접근해 name, age 프로퍼티에 각각 값을 대입한다.

출력 결과를 보면 각각의 이름과 나이가 잘 대입된 것을 확인할 수 있다.


따라서 생성자 함수 내부의 this는 인스턴스 자신이 되는 것을 알 수 있다.




this 바인딩 방법

앞에서 this가 바인딩되는 여러 상황을 살펴봤지만, 직접 별도의 대상을 바인딩하는 방법도 존재한다.



1. call 메서드

call 메서드는 호출 주체인 함수를 즉시 실행하도록 하는 명령이다.

Function.prototype.call(thisArg[, arg1[, arg2[, ...]]])

첫 번째 인자를 this로 바인딩하고, 이후의 인자들을 호출할 함수의 매개변수로 사용한다.

var func = function (a, b, c) {
  console.log(this, a, b, c);
};

func(1, 2, 3); // Window { ... } 1 2 3
func.call({ x: 1 }, 1, 2, 3); // { x : 1 } 1 2 3
var obj = {
  a: 1,
  method: function (x, y) {
    console.log(this.a, x, y);
  },
};

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

위 코드에서 확인할 수 있듯이, 임의의 객체를 this로 지정할 수 있다.



2. apply 메서드

apply 메서드는 call 메서드와 기능적으로 동일하지만, 두 번째 인자를 배열로 받아 함수의 매개변수로 사용한다는 점이 다르다.

Function.prototype.apply(thisArg[, argsArray]);
var func = function (a, b, c) {
  console.log(this, a, b, c);
};

func(1, 2, 3); // Window { ... } 1 2 3
func.apply({ x: 1 }, [1, 2, 3]); // { x : 1 } 1 2 3
var obj = {
  a: 1,
  method: function (x, y) {
    console.log(this.a, x, y);
  },
};

obj.method(2, 3); // 1 2 3
obj.method.call({ a: 10 }, [2, 3]); // 10 2 3



3. bind 메서드

bind 메서드는 call 메서드와 비슷하지만 즉시 함수를 호출하지는 않고, 넘겨받은 this 및 인수들을 바탕으로 새로운 함수를 반환하기만 하는 메서드이다.

Function.prototype.bind(thisArg[, arg1[, arg2[, ...]]])
var func = function (a, b, c) {
  console.log(this, a, b, c);
};

func(1, 2, 3); // Window { ... } 1 2 3

var bindFunc1 = func.bind({ x: 1 }, 1, 2, 3);
bindFunc1(1, 2, 3); // { x : 1 } 1 2 3

// 매개변수 부분 적용도 가능하다.
var bindFunc2 = func.bind({ x: 1 }, 1);
bindFunc2(2, 3); // { x : 1 } 1 2 3


bind 메서드를 사용하여 만든 함수는 name 프로퍼티 앞에 bound 접두어가 붙는 특이점이 있다.

var func = function (a, b, c) {
  console.log(this, a, b, c);
};

var bindFunc = func.bind({ x: 1 }, 1, 2, 3);

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



4. 화살표 함수 (ES6+)

화살표 함수는 this 바인딩 과정이 생략되기 때문에, 스코프 상 가장 가까운 this 에 접근한다.

var obj = {
  outer: function () {
    console.log(this); // obj { ... }

    var innerFunc = () => {
      // 화살표 함수
      console.log(this); // obj { ... }
    };

    innerFunc();
  },
};

obj.outer();



5. 별도의 인자를 this로 받는 경우(콜백 함수)

콜백 함수를 인자로 받는 메서드 중 일부는 추가로 this로 지정할 객체를 인자로 지정할 수 있는 경우가 있다.


5-1. 콜백 함수와 함께 thisArg를 인자로 받는 메서드

  • Array.prototype.forEach(callback[, thisArg])
  • Array.prototype.map(callback[, thisArg])
  • Array.prototype.filter(callback[, thisArg])
  • Array.prototype.some(callback[, thisArg])
  • Array.prototype.every(callback[, thisArg])
  • Array.prototype.find(callback[, thisArg])
  • Array.prototype.findIndex(callback[, thisArg])
  • Array.prototype.flatMap(callback[, thisArg])
  • Array.prototype.from(arrayLike[, callback[, thisArg]])
  • Set.prototype.forEach(callback[, thisArg])
  • Map.prototype.forEach(callback[, thisArg])
// [예시 코드]

var report = {
  sum: 0,
  count: 0,
  add: function () {
    var args = Array.prototype.slice.call(arguments);

    args.forEach(function (entry) {
      this.sum += entry;
      ++this.count;
    }, this); // thisArg를 인자로 받을 수 있다.
  },
  average: function () {
    return this.sum / this.count;
  },
};

report.add(50, 40, 30); // => this인 report도 함께 전달
console.log(report.sum); // 120
console.log(report.count); // 3
console.log(report.average()); // 40

콜백 함수 내부에서의 this는 add 메서드의 thisreport가 전달된다.

따라서 this.sumreport.sum과 동일하며, 의도한 연산이 이뤄지게 된다.




요약

다음은 this 바인딩이 없는 한 늘 성립하는 규칙이다

  • 전역 공간에서의 this전역객체를 참조한다.
  • 어떤 함수를 메서드로서 호출한 경우 this메서드 호출 주체를 참조한다.
  • 어떤 함수를 함수로서 호출한 경우, this전역 객체를 참고한다.
  • 콜백 함수 내부에서의 this해당 콜백 함수의 제어권을 넘겨받은 함수가 정의한 바에 따르며, 정의하지 않은 경우에는 전역 객체를 참조한다.
  • 생성자 함수에서의 this생성될 인스턴스를 참조한다.


다음은 명시적 this 바인딩 규칙이다.

  • call, apply 메서드는 this명시적으로 지정할 수 있다.
  • bind 메서드는 this 및 함수에 넘길 인수를 일부 지정해 새로운 함수를 만든다.
  • 요소를 순회하며 콜백 함수를 반복 호출하는 내용의 일부 메서드는 별도의 인자로 this를 받기도 한다.





출처

코어 자바스크립트 3장

댓글남기기