본문 바로가기
  • soldonii's devlog
Javascript 공부/TIL

200117(금) : this

by soldonii 2020. 1. 17.

1. This

this가 무엇인지 정확하게 알기 위해서, 죽을 때까지 아래 세 문장만큼은 잊지 말자!

this는 1) 함수 자신을 가리키는 것도 아니고, 2) 함수 내부의 스코프를 가리키는 것도 아니다!!
this는 '함수 호출 시점'에 바인딩되며, this가 무엇을 가리킬지는 전적으로 '함수를 호출하는 방식'에 달려있다!!
따라서 1) this가 포함된 함수 호출 스택을 추적해서 함수 호출 시점을 파악한 후, 2) 해당 함수가 어떤 방식으로 호출되었는지를 확인해야 한다!!

 

1) 호출부

this가 무엇을 가리킬지 이해하기 위해서는 '함수를 호출한 지점'을 확인하면 될 것 같지만, 생각보다 쉽지 않다. 중요한 것은 '호출 스택(현재 실행 지점에 오기까지 호출된 함수의 스택)'을 생각해야 한다.

function baz() {
  console.log('baz');
  bar();
}

function bar() {
  console.log('bar')
  foo();
}

function foo() {
  console.log('foo');
}

baz();
  • baz는 global scope에서, barbaz 내부에서(bar의 호출부는 baz 내부), foobar 내부에서(foo의 호출부는 bar 내부) 호출됐다.
  • 호출 스택은 baz -> bar -> foo 이다.
  • 호출 스택을 찾는 것은 이론적 내용이 중요한 것이 아니라, 무수한 연습을 거쳐야 한다. 크롬의 debugger 툴을 이용하면 조금이나마 간편하다.

 

2) this 바인딩의 4가지 원칙

(1) 기본 바인딩
  • 가장 평범한 함수 호출인 '단독 함수 실행'으로 호출될 경우(ex. foo()) this는 global 객체를 가리킨다.
  • 다만 엄격모드에서 전역 객체는 기본 바인딩 대상에서 제외되기 때문에 thisundefined가 된다.
  • 주의할 점은, foo() 호출부의 엄격모드 여부는 상관이 없다.
// 1. 엄격 모드에서 전역객체는 바인딩이 되지 않는다. 따라서 this는 undefined.
function foo() {
  "use strict"
  console.log(this.a);
}

let a = 2;
foo(); // TypeError: 'this' is 'undefined'.

// 2. 하지만 함수 호출부가 엄격모드인 것은 상관 없다.
function foo() {
  console.log(this.a);
}

let a = 2;
(function () {
  "use strict"
  foo(); // 2
})();

 

(2) 암시적 바인딩
  • 함수 호출부에 콘텍스트 객체가 존재하는지, 즉 객체의 소유/포함 여부를 확인하는 것이다.
  • 호출부에서 어떤 객체를 통해 함수를 참조하고 있는지를 확인한 후, 해당 객체가 확인되면 해당 객체가 this로 바인딩 되는 것이다.
function bar() {
  console.log(this.num);
}

let obj = {
  num: 2,
  bar: bar
};

obj.bar(); // 2

 

  1. 함수의 호출부는 전역 객체이다.
  2. 전역 객체에서 함수 foo가 어떤 객체를 통해서 참조되고 있는지를 살펴본다.
  3. 함수 foo는 객체 obj를 통해서 참조되고 있다.(즉, 함수 foo를 소유/포함하고 있는 객체는 obj이다.)
  4. 따라서 obj.foo()에서 this는 obj 객체로 바인딩되고, 따라서 this.a는 obj.a가 되므로 2를 로그하게 된다.
  •  
  • 만약 객체 프로퍼티 참조가 체이닝된 형태라면, 최상위/최하위 정보만 호출부와 연관된다.

function bar() {
  console.log(this.num);
}

let obj2 = {
  num: 30,
  bar: bar
};

let obj3 = {
  num: 111,
  obj2: obj2
};

obj3.obj2.bar(); // 30, 체이닝 호출의 경우 중간단계인 obj2.a의 값은 무시된다.
function foo() {
  console.log(this.a);
}

function doFoo(fn) {
  fn();
}

let obj = {
  a: 2,
  foo: foo
};

let a = 'this is global a.';
doFoo(obj.foo);

 

(3) 명시적 바인딩
  • call(), apply() 메소드를 이용하여, 메소드의 첫번째 인자로 바인딩할 객체를 직접 세팅할 수 있다.
  • 박싱 : 객체 대신 단순 원시값(문자열, 불리언, 숫자 등)을 인자로 전달할 경우, 원시값에 대응되는 객체(ex. new String())으로 래핑된다.

[하드 바인딩]

function foo() {
  console.log(this.a);
}
let obj = {
  a: 2
};
let bar = function () {
  foo.call(obj);
};

bar(); // 2
setTimeout(bar, 100); // 2

// bar는 이미 하드바인딩 되었기 때문에 명시적으로 인자를 전달하여도 해당 인자는 this로 바인딩 되지 않는다.
bar.call(window); // 2, window는 this로 바인딩되지 않는다.

 

(4) new 바인딩

new 바인딩을 이해하기 위해, new를 이용한 '생성자 호출'시 어떤 일이 일어나는지 살펴보자.

 

  1. 새로운 객체가 만들어진다.
  2. 새로 생성된 객체의 [[Prototype]] 링크가 연결된다.
  3. 새로 생성된 객체는 해당 함수 호출 시 this로 바인딩 된다.
  4. 이 함수가 자신의 또 다른 객체를 반환하지 않는 한, new와 함께 호출된 함수는 자동으로 새로 생성된 객체를 반환한다.
  •  

결국 new는 함수 호출 시 this를 새로운 객체와 바인딩하는 방법이며, 이것이 new 바인딩이다.

 

3) this 바인딩 규칙의 우선순위

[ 명시적 바인딩(call, apply, bind) > new 바인딩 > 암시적 바인딩 > 기본 바인딩 ] 순서이다. 앞 쪽일수록 우선순위가 높다.

 

4) 바인딩 예외

  • call, apply, bind 메소드의 첫 번째 인자로 null, undefined를 넘길 경우 this 바인딩이 무시되고 기본 바인딩 규칙이 적용된다.
  • 그러나 this 바인딩이 어떻든 상관없다는 이유로 null을 즐겨쓰는 것은 약간의 risk가 있다. 특정 함수 호출 시 null을 적용했는데, 그 함수가 내부적으로 this를 참조할 경우에는 최악의 상황에는 global 객체(ex. window)를 가리킬 수 있고, 매우 큰 버그가 발생할 수 있기 때문이다.
  • 더 안전하게 하기 위해서는, Object.create(null)을 이용하여 아예 완전히 텅터어 빈 객체를 null 대신 전달하는 편이 낫다.

 

5) 어휘적 this(화살표 함수 사용)

일반적인 함수는 모두 4가지 규칙을 준수하지만, ES6부터 적용되는 화살표 함수를 사용할 경우, 4가지 규칙 대신 Lexical Scope를 참조하여 this를 알아서 바인딩한다.

 

this의 경우 2번 정도 [You Don't Know JS] 책을 읽으면서 정리한 것인데도 명확하게 이해가 됐다고 보긴 어려운 듯 하다.... 계속 읽어보고 this를 수없이 사용해봐야 감이 잡힐 듯 하다.

댓글