this — 어디서 만들었나가 아니라 어떻게 불렀나

같은 함수인데 어떤 때는 객체를, 어떤 때는 undefined 를 가리키는 this. 함수를 부르는 모양 네 가지만 보면 값이 정해진다.

Seobway · · 8분

같은 함수인데 this 가 바뀐다

함수는 하나다. 부르는 모양만 바꿨다. 그런데 결과가 다르다.

const user = {
  name: "민수",
  hello() { return `안녕, ${this.name}`; },
};

user.hello();            // "안녕, 민수"

const fn = user.hello;   // 같은 함수를 변수에 옮겨 담았을 뿐
fn();                    // TypeError: Cannot read properties of undefined

fn 과 user.hello 는 완전히 같은 함수다(fn === user.hello 는 true).
달라진 건 부른 모양뿐이다. this 는 그 모양을 보고 정해진다.


어떻게 동작하는가

실행 컨텍스트에서 컨텍스트가 담는 것 세 가지를 봤다.
환경 레코드, 바깥 환경 참조, 그리고 this. 셋 중 둘은 코드를 쓴 위치로 정해지는데,
this 만 다르다.

그러니 this 를 알고 싶으면 함수 정의를 볼 게 아니라 호출하는 줄을 봐야 한다.
호출 모양은 네 가지뿐이다.

객체.메서드() — 점 앞의 객체가 this 다.

user.hello();          // this = user
a.b.c.hello();         // this = a.b.c  (바로 앞 하나만 본다)

함수가 어느 객체 안에 적혀 있는지는 상관없다. 호출할 때 점 앞에 뭐가 있느냐만 본다.

여러 개가 겹치면 new > bind·call·apply > 점 찍고 부르기 > 그냥 부르기 순으로 이긴다.

인터랙티브 다이어그램: 같은 함수를 세 가지 모양으로 부른다 (단계별로 재생할 수 있다)

  1. user.hello() — 점 앞에 user 가 있다.
  2. 호출하는 순간 컨텍스트가 만들어지고 this 에 user 가 들어간다.
  3. 함수가 끝나면 컨텍스트가 빠진다. this 도 같이 사라진다.
  4. fn() — 같은 함수지만 점 앞이 비어 있다.
  5. 이번 호출의 컨텍스트에는 this 로 undefined 가 들어간다.
  6. 끝나서 빠지고, fn.call(user) — 이번엔 호출하면서 this 를 직접 넣는다.
  7. 같은 함수인데 이번 컨텍스트의 this 는 user 다.

직접 확인

whoAmI 는 자기 this 가 누군지 말하는 함수다. 호출 모양만 바꿔 가며 불러 본다.

function whoAmI() {
  "use strict";
  return this === undefined ? "undefined" : this.label;
}

const a = { label: "a", whoAmI };
const b = { label: "b", whoAmI };

console.log("a.whoAmI()          →", a.whoAmI());
console.log("b.whoAmI()          →", b.whoAmI());   // 같은 함수, 다른 this
console.log("whoAmI()            →", whoAmI());

const taken = a.whoAmI;                            // 꺼내 담으면
console.log("taken()             →", taken());     // a 를 잃는다

console.log("whoAmI.call(b)      →", whoAmI.call(b));

const fixed = whoAmI.bind(a);
b.fixed = fixed;
console.log("b.fixed() (bind a)  →", b.fixed());   // bind 가 점보다 이긴다

// 화살표 함수는 자기 this 가 없다 — 바깥 this 를 그대로 쓴다
const c = {
  label: "c",
  later() {
    return [1].map(() => this.label)[0];
  },
};
console.log("화살표 안의 this    →", c.later());

a.whoAmI() 와 b.whoAmI() 는 같은 함수를 부른다. 함수가 a 안에 먼저 적혔다는
사실은 아무 의미가 없다.


흔한 실수


한 줄 정리

this 는 함수를 정의할 때가 아니라 호출할 때, 호출하는 모양을 보고 정해진다.
점 앞의 객체 · undefined · call/bind 로 넣은 값 · new 가 만든 새 객체 — 네 가지 중 하나다.

참고

  1. [1] this — MDN Web Docs
  2. [2] Function.prototype.bind() — MDN Web Docs
  3. [3] OrdinaryCallBindThis — ECMAScript 사양

관련 글