서론

자바스크립트에서의 this는 동적으로 정해지기 때문에 헷갈리기로 악명높다. 다행히 쉽게 쉽게 설명해주는 자료가 많이 있기때문에 이해하기 어렵지 않고, 실제 사용에도 문제는 없다.

다만, 쉬운 설명은 쉬운 설명일 뿐 정확한 설명은 아니다. 흔히 객체의 메소드 형식-foo.bar()으로 함수가 실행된다면 this는 객체-foo가 된다.라고 설명을 하는데. 이런식으로 this를 이해한다면 아래 예시의 결과를 설명할 수 없을것이다.

x = 'global x';

var foo = {
  x : 'foo x',
  getX : function () {
    console.log(this.x);
  }
};

foo.getX();   // foo x
(foo.getX)(); // foo x

(false || foo.getX)();  // ???
(foo.getX, foo.getX)(); // ???

마지막 2개의 예시는 foo x가 아니라 global x가 출력된다.

분명 foo.getX 형식으로 함수를 호출했는데 왜 thisglobal로 정해진걸까?

ECMAscript의 Reference Type

자바스크립트에는 내부적으로 사용하는 Reference Type이라는 것이 존재한다.

Reference Type이 무엇인지 알아보기 전에, Reference Type이 발생하는 경우를 먼저 알아보자.

Reference Type이 발생하는 경우

  • Identifier 또는 Property Accessor를 평가했을 때

Identifier, Property Accessor 라는 용어를 쓰니까 어려워 보이지만, '변수의 이름을 사용해 접근하는 경우' 라고 생각하면 간단하다.

function func() {

};

var a = 1;
var foo = { bar: 2};

func();             // identifier
a = 111;            // identifier

foo.bar = 222;      // property accessor 
foo['bar'] = 222;   // property accessor

위의 예시의 func();, a = 111;, foo.bar = 222;처럼 '변수의 이름을 사용해서' 어떤 값에 접근한다면, 자바스크립트 엔진은 그 자리에 Reference Type을 반환한다.

Reference Type의 구조

Reference Type은 base, propertyName, strict 3가지 컴포넌트(속성)를 가지고 있다. 위 예시의 foo.bar를 Reference Type으로 변환하면 다음과 같다.

foo.bar = 222;    // property accessor dot(.)을 사용했으므로 Reference Type생성

barReference = {
  base: foo,
  propertyName: 'bar',
  strict: false,
}

//strict는 실행중인 코드에 'use strict'가 적용되어있는지를 나타내는 플래그다.

메소드 호출의 Reference Type

Identifier, Property Accessor를 사용하면 Reference가 생성되므로, 할당(Assignment)뿐만 아니라 함수의 호출에서도 Reference가 사용된다.

여기서 중요한 부분은 함수를 호출할 때 생성되는 Reference의 basethis값을 결정한다는 것이다.

var foo = {
  x: 123,
  bar: function () {
    console.log(this.x);
  }
};

foo.bar();    // base가 foo이므로 this는 foo가 된다.

barReference = {
  base: foo,
  propertyName: 'bar',
  strict: false,
}

base가 일반 object일 경우 this는 해당 object로 정해진다.

단일 함수 호출의 Reference

그렇다면 메소드 형태가 아닌 단일 함수 형태의 Reference는 어떻게 생겼을까?

1. global 영역 함수의 Reference

function bar() {
  console.log(this);  // global
}

bar();    // base === ?

barReference = {
  base: GlobalEnvironmentRecord,   
  propertyName: 'bar',
  strict: false,
}

???

난데없이 등장한 GlobalEnvironmentRecord의 정체는 무엇일까?

자바스크립트의 실행 콘텍스트를 공부해봤다면 변수 객체(Variable Object)라는 용어를 들어봤을 것이다.

GlobalEnvironmentRecord는 변수 객체(Variable Object)의 새로운 이름이다. (ES5부터 변경됨)

(간단하게, 실행중인 (전역) 스코프의 모든 변수 이름(식별자, identifier)이 등록되는 곳이라 이해하면 된다.)

여튼 bar라는 식별자를 이용해 함수를 호출하므로 barReference가 생성되며, base는 GlobalEnvironmentRecord가 된다.

base가 일반 object일 경우 this는 해당 object로 지정된다고 언급했는데, base가 EnvironmentRecord인 경우에는 딱 한가지 경우를 제외하고 thisundefined로 지정되어 함수가 호출된다.

여기까지 읽었다면 아마 base가 Global Environment Record면, this가 global object로 정해지겠구나!' 라고 예상할 것이다.

안타깝게도 틀렸다. base가 GlobalEnvironmentRecord라고 해도 this는 undefined로 전달된다.

ECMA-262-9 명세 : 12.3.4.2 Runtime Semantics: EvaluateCall를 보면 함수 호출식을 평가하는 과정에서 base가 EnvrionmentRecord일 경우 ThisValue를 EnvrionmentRecord.WithBaseObject()로 결정한 뒤 함수를 호출한다고 명시하고 있다.

WithBaseObject() 항목을 살펴보면 EnvironmentRecord가 with statement와 연관이 있다면 with object를 리턴하고, 아닌 경우에는 undefined를 리턴한다고 명시하고 있다.

<중요> this의 값이 undefined로 지정되어 함수가 호출된다고 해서, this === undefined인 것은 아니다. strict mode가 아니라면 undefined로 지정된 this는 자동으로 전역 객체(window/global)로 변환된다. 다음 글에서 함수의 실행컨텍스트 생성과정에서 this가 어떻게 결정되는지 알아본다.

2. nested function의 Reference

function foo() {
  function nested() {
    console.log(this);  // global
  }

  nested();  // base === ?
}

foo();    

nestedReference = {
  base: fooEnvironmentRecord,   
  propertyName: 'bar',
  strict: false,
}

foo함수 안에서 생성되고 실행된 nested함수는 foo함수의 (실행컨텍스트의) EnvironmentRecord를 base로 가진채 실행되며

base가 EnvironmentRecord이기 때문에 마찬가지로 this는 undefined로 지정되어 실행된다.

간단한 결론

(with statement를 사용하는 경우를 제외하고) base가 EnvironmentRecord일 경우에는 this는 undefined로 지정되어 함수가 호출된다.

즉 (propery accessor 없이) 단일 식별자로 함수를 호출하면, 무조건 thisundefined로 지정되어 실행되고, strict모드가 아니라면 global로 변환된다는 말이 된다.

var foo = {
  bar: function() {
    console.log(this)       // foo

    function innerFunc() {
      console.log(this);    // ???
    }

    innerFunc();

    /*
      innerFuncReference = {
        base: barEnvironmentRecord,     // this -> undefined -> global
        propertyName: 'innerFunc',
      }
    */
  }
};

foo.bar();

위의 예시를 보면 foo.bar 메소드는 this가 foo로 바인딩 되어있다.

하지만 bar 내부에서 innferFunc를 생성해서 실행하더라도, innerFunc의 base가 barEnvironmentRecord이기 때문에 innerFunc의 this는 foo가 아니라 undefined -> global로 결정된다.

3. Reference Type이 아닌 함수의 호출

(function() {
  console.log(this);    // global
})();

즉시 실행 함수(IIFE)의 경우에는 identifer 혹은 property accessor를 사용한 경우가 아니므로 reference type이 아니라 function object 자체가 떨어지게 된다.

12.3.4.2Runtime Semantics: EvaluateCall의 step2를 보면 Reference가 아닌 경우에 ThisValue는 undefined로 지정한다고 명시되어있다.

마무리

여기까지 잘 따라왔다면, Reference Type에 대한 이해를 바탕으로 함수의 호출 형태를 보고 this가 어떻게 결정될지 근거를 가지고 예측할 수 있게 되었을 것이다.

본문에서 this가 undefined로 지정되어 함수가 호출되면 this가 global로 자동변환된다고 대충 설명하고 넘어갔는데, 다음 글에서는 어떻게 undefined로 지정된 this가 global로 설정되는지, 화살표 함수에서 사용되는 this는 어떻게 결정되는지를 알아보려고 한다.

끝으로 글 서두에 어그로를 끌기위해 심어두었던 예제를 설명하고 마무리하도록 하겠다 ㅋㅋㅋ;

x = 'global x';

var foo = {
  x : 'foo x',
  getX : function () {
    console.log(this.x);
  }
};

foo.getX();   // 1) foo x
(foo.getX)(); // 2) foo x

(false || foo.getX)();  // 3) ???
(foo.getX, foo.getX)(); // 4) ???

결론만 얘기하자면 마지막 3,4번의 예제가 global이 되는 이유는 Reference Type이 파괴되었기 때문이다.

2번에 적용된 괄호() grouping operator는 내부 표현식의 평가를 거쳐도 Reference Type을 그대로 반환한다.

반면에 comma(,)와 or(||) 연산자의 경우에는 피연산자에 internal method인 getValue()를 사용해서 실제값을 반환하게된다.

따라서 3,4번의 경우에는 foo.getX의 Reference Type이 , ||연산을 거치면서 function object로 변환되었기 때문에 this가 undefined로 지정되어 실행된 것이다.

참고자료

ECMA-262-9.0

ECMA-262-3 in detail. Chapter 3. This.

Know thy reference