원문 출처 : ECMA-262-3 in detail. Chapter 3. This. by Dmitry Soshnikov
 
 
 
소개(Introduction)


이 글에서는 실행 콘텍스트와 관련있는 내용을 더 자세하게 알아볼 것이다.
이번 주제는 this 키워드다. 사례에서 보듯이, 이 주제는 상당히 어려워서 종종 다른 실행 콘텍스트의 this 값을 처리할 때 이슈를 만들곤 한다.
많은 프로그래머가 프로그래밍 언어의 this 키워드가 객체 지향 프로그래밍과 관련이 있다고 생각한다. 정확하게는 생성자를 이용해서 새롭게 만들어진 객체를 참조한다고 생각한다. ECMAScript에도 이 개념은 정의되어 있다. 그러나 앞으로 보게 될 것처럼, ECMAScript는 this의 정의를 오직 생성한 객체로 제한하지 않는다.
자, 그러면 ECMAScript에서 사용하는 this의 정확한 의미를 살펴보자.
 
 
정의(Definitions)


this는 실행 콘텍스트의 프로퍼티다.
 

activeExecutionContext = {
  VO: {...},
  this: thisValue
};

 
여기의 VO는 이전 장에서 설명했던 변수 객체(Variable object)를 의미한다.
this는 콘텍스트의 실행 코드 타입과 직접적인 관련이 있다. 이 값은 콘텍스트로 진입하는 과정에서 정해지며, 콘텍스트 안의 코드가 실행 중에는 변하지 않는다.
좀 더 자세하게 들여다보자.
 
 
전역 코드 안의 this(This value in the global code)


이 경우는 모든 것이 아주 단순하다. 전역 코드 안에 있는 this는 항상 전역 객체 자신이다. 그 결과, 간접적으로 참조할 수 있다.
 

// 명시적인 전역 객체 프로퍼티 정의
this.a = 10; // global.a = 10
alert(a); // 10
// 규정되지 않은 식별자 할당을 이용한 암묵적 정의
b = 20;
alert(this.b); // 20
// 전역 콘텍스트의 변수 객체는 전역 객체 자신이기 때문에
// 또한 변수 선언을 이용한 암묵적 정의도 가능하다.
var c = 30;
alert(this.c); // 30

 
 
함수 코드 안의 this(This value in the function code)


더 흥미로운 것은 this가 함수 코드 안에서 사용될 때이다. 이 경우가 가장 이해하기 어렵고 많은 문제를 일으킨다.
함수 타입의 코드의 this가 갖는 첫번째 특징(그리고 아마 가장 주된 기능)은 함수에 정적으로 바인딩되지 않는 다는 것이다.
위에서 언급을 했듯이, this는 콘텍스트로 들어갈 때 결정이 되는데, 함수 코드의 경우에는 this가 가르키는 대상이 매번 완전히 다를 수 있다.
그렇지만, 코드 실행 시에는 this 값이 변하지 않는다. 즉, 다시 말해서 this는 변수가 아니므로 여기에 새로운 값을 할당하는 것이 불가능 하다는 이야기다(반대로, Python에서 명시적으로 정의한 self 객체는 런타임시에 반복적으로 변할 수 있다).
 

var foo = {x: 10};
var bar = {
  x: 20,
  test: function () {
    alert(this === bar); // true
    alert(this.x); // 20
    this = foo; // 에러, this 값을 변경할 수 없다.
    alert(this.x); // // 만약 위에서 에러가 나지 않았다면 20이 아닌 10이 출력될 것이다.
  }
};
// 콘텍스트로 들어올 때 this가 가리키는 대상이 bar 객체로 결정된다.
// 왜 그러한지는 아래에서 자세하게 설명하겠다.
bar.test(); // true, 20
foo.test = bar.test;
// 그러나 여기의 this는 이제 foo를 참조할 것이다.
// 심지어 같은 함수를 호출하는 데도 말이다.
foo.test(); // false, 10

 
그렇다면 함수 코드 안의 this 값을 변하게 만드는 것은 무엇일까? 다양한 요인이 존재한다.
우선, 일반적인 함수 호출의 this는 콘텍스트의 코드를 활성화시킨 호출자(caller)에 의해 제공된다. 즉, 함수를 호출한 부모 콘텍스트가 제공하는 것이다. 그리고 this가 갖는 값은 호출 표현식의 형태에 의해서 정해진다(다른 말로 하자면, 구문적으로 어떤 형태로 함수를 호출 했는가에 따라서 결정된다).
어떤 콘텍스트의 this가 참조하는 값을 어려움없이 알아내기를 원한다면 중요한 이 부분을 이해하고 기억해야 한다. 정확하게 호출 표현식의 형태, 그러니까 다른 무엇이 아닌 함수를 호출한 방법이 호출된 콘텍스트의 this 값에 영향을 준다.
Javascript에 대한 몇몇 글이나 심지어 책에서 다음과 같이 주장하는 것을 볼 수 있다.
 

“this 값은 함수가 어떻게 정의되었는가에 따라 정해진다. 전역 함수라면 this는 전역 객체를 값으로 갖게 되고, 객체의 메서드라면 this는 항상 이 객체를 값으로 갖는다.”

 
이는 잘못된 설명이다. 앞으로 나아가서, 심지어 보통의 전역 함수도 다른 형태의 호출 표현식으로 활성화되면 this 값이 달라진다.
 

function foo() {
  alert(this);
}
foo(); // global
alert(foo === foo.prototype.constructor); // true
// 그러나 같은 함수에 대한 호출 표현식을 또 다른 형태로 이용하면,
// 다른 this 값을 갖게 된다.
foo.prototype.constructor(); // foo.prototype

 
이것과 유사하게, 어떤 객체의 메서드로 정의된 함수를 호출하는 경우에도 this는 이 객체를 고정된 값으로 갖지 않는다.
 

var foo = {
  bar: function () {
    alert(this);
    alert(this === foo);
  }
};
foo.bar(); // foo, true
var exampleFunc = foo.bar;
alert(exampleFunc === foo.bar); // true
// 같은 함수를 또 다른 형태의 호출 표현식으로 부르게 되면,
// this 값이 변한다.
exampleFunc(); // global, false

 
그렇다면 호출 표현식의 형태가 어떻게 this에 영향을 미칠까? this가 갖는 값을 결정하는 과정을 완벽하게 이해하기 위해서, 내부 타입 중에 하나인 레퍼런스 타입(Reference type)에 대해서 자세하게 알 필요가 있다.
 
 
레퍼런스 타입(Reference type)


레퍼런스 타입은 수도 코드를 이용해서 base(프로퍼티가 속해 있는 객체)와 이 base 안에 있는 propertyName이라는 2개의 프로퍼티를 갖고 있는 객체로 나타낼 수 있다.
 

var valueOfReferenceType = {
  base: <base object>,
  propertyName: <property name>
};

 
레퍼런스 타입의 값은 오직 아래의 2가지 경우에만 있을 수 있다.
 

  1. 식별자(identifier)를 다룰 때
  2. 또는 프로퍼티 접근자(property accessor)를 다룰 때.

 
식별자는 식별자 처리 프로세스에 따라서 처리가 되는데 이에 대한 자세한 내용은 Chapter4. Scope chain에서 다루겠다. 여기에서는 단지 이 알고리즘이 항상 레퍼런스 타입 값(this와 관련해서 중요하다)을 결과로 돌려준다는 것만 명심하자.
식별자는 변수 이름, 함수 이름, 함수 전달인자의 이름 그리고 전역 객체의 비정규화 프로퍼티의 이름을 뜻한다.
예를 들어, 다음과 같은 식별자의 값이 있을 때,
 

var foo = 10;
function bar() {}

 
위 연산의 중간 결과는, 아래의 레퍼런스 타입 값과 같다.
 

var fooReference = {
  base: global,
  propertyName: 'foo'
};
var barReference = {
  base: global,
  propertyName: 'bar'
};

 
레퍼런스 타입 값으로부터 객체가 갖고 있는 실제 값을 얻기 위해 쓰이는 GetValue 메소드가 있는데 이 메소드를 수도 코드로 아래와 같이 나타낼 수 있다.
 

function GetValue(value) {
  if (Type(value) != Reference) {
    return value;    // 레퍼런스 타입이 아니면 값을 그대로 돌려준다.
  }
  var base = GetBase(value);
  if (base === null) {
    throw new ReferenceError;
  }
  return base.[[Get]](GetPropertyName(value));
}

 
위에서 내부 [[Get]] 메소드는 프로토타입 체인으로부터 상속된 프로퍼티까지 분석해서 객체 프로퍼티의 실제 값을 돌려준다.
 

GetValue(fooReference); // 10
GetValue(barReference); // function object "bar"

 
또한 프로퍼티 접근자는 점 표기법(프로퍼티 이름이 정확한 식별자이고 미리 알 수 있을 때)이나 대괄호 표기법의 2가지 방법으로 표기할 수 있다.
 

foo.bar();
foo['bar']();

 
이번에도 중간 계산의 결과로 레퍼런스 타입의 값을 갖게 된다.
 

var fooBarReference = {
  base: foo,
  propertyName: 'bar'
};
GetValue(fooBarReference); // function object "bar"

 
그렇다면, 레퍼런스 타입의 값과 함수 콘텍스트의 this 값은 어떤 관계일까? 이 부분이 가장 중요하며, 이 글의 메인이다. 함수 콘텍스트의 this 값을 결정하는 일반적인 규칙은 다음과 같이 말할 수 있다.
 

함수 콘텍스트의 this 값은 호출자가 제공하며 호출 표현식의 현재 형태에 의해서 그 값이 결정된다(함수 호출이 문법적으로 어떻게 이뤄졌는지에 따라서).
 
호출 괄호 (…)의 왼편에 레퍼런스 타입의 값이 존재하면, this는 레퍼런스 타입의 this 값인 base 객체를 값으로 갖는다. 
 
다른 모든 경우에는(레퍼런스 타입이 없는 다른 모든 값의 경우), this 값은 항상 null로 설정된다. 그러나 null은 this의 값으로 의미가 없기 때문에 암묵적으로 전역 객체로 변환된다. 

 
예제를 살펴보자.
 

function foo() {
  return this;
}
foo(); // global

 
호출 괄호의 왼편에 레퍼런스 타입 값이 있다(foo는 식별자이기 때문이다).
 

var fooReference = {
  base: global,
  propertyName: 'foo'
};

 
따라서, this 값은 레퍼런스 타입 값의 base 객체인 전역 객체로 설정된다.
프로퍼티 접근자의 경우도 비슷하다.
 

var foo = {
  bar: function () {
    return this;
  }
};
foo.bar(); // foo

 
여기에서 다시 base가 foo 객체인 레퍼런스 타입의 값을 갖게 되고, 이것은 bar 함수 활성화 시에 this 값으로 이용된다.
 

var fooBarReference = {
  base: foo,
  propertyName: 'bar'
};

 
그러나, 또 다른 형태의 호출 표현식으로 함수를 활성화시키면 this 값은 달라진다.
 

var test = foo.bar;
test(); // global

 
test가 식별자가 되면서 다른 레퍼런스 타입 값을 만들기 때문에, 이 레퍼런스 타입의 base(전역 객체)가 this 값으로 사용된다.
 

var testReference = {
  base: global,
  propertyName: 'test'
};

 

ES5의 strict mode의 this 값은 전역 객체로 강제 변환되지 않는다. 대신에 undefined 값이 할당된다.

 
이제 왜 다른 형태의 호출 표현식으로 활성화된 같은 함수가, 또한 다른 this 값을 갖는지를 정확하게 이야기할 수 있다. – 답은 레퍼런스 타입의 중간 값이 다르다는 데에 있다.
 

function foo() {
  alert(this);
}
foo(); // 전역이기 때문에
var fooReference = {
  base: global,
  propertyName: 'foo'
};
alert(foo === foo.prototype.constructor); // true
// 호출 표현식의 또 다른 형태
foo.prototype.constructor(); // foo.prototype이기 때문에
var fooPrototypeConstructorReference = {
  base: foo.prototype,
  propertyName: 'constructor'
};

 
호출 표현식의 형태에 따라 this 값이 동적으로 결정되는 또 다른 예제(일반적으로 인용되는)가 있다.
 

function foo() {
  alert(this.bar);
}
var x = {bar: 10};
var y = {bar: 20};
x.test = foo;
y.test = foo;
x.test(); // 10
y.test(); // 20

 
 
 
[역주]
ECMA-262-3은 레퍼런스 타입을 아래와 같이 설명하고 있다.
 

“내부 레퍼런스 타입은 프로그래밍 언어의 데이터 타입이 아니다. 이것은 설명을 목적으로 ECMA 스펙이 정의하고 있는 개념이다. ECMAScript 구현체는 여기에 설명되어 있는 것처럼 레퍼런스를 만들고 처리해야 한다. 그러나 표현식을 평가한 중간 결과로서만 레퍼런스 타입의 값을 이용할 수 있고 프로퍼티나 변수에 저장할 수는 없다.
레퍼런스 타입은 delete, typeof 그리고 할당 연산자와 같은 연산자의 동작을 설명하는 데 이용한다. 예를 들어, 할당 연산자의 좌측 피연산자는 레퍼런스를 생성한다. 할당 연산자 좌측의 피연산자는 구문 형태 분석의 관점으로 설명할 수 있지만, 레퍼런스를 반환하는 함수 호출의 경우는 설명하기 어렵다. 이는 호스트 객체 때문이다. ECMA 스펙이 정의하고 있는 어떠한 내장 함수도 레퍼런스를 반환하지 않으며, 레퍼런스를 반환하는 사용자 정의 함수에 대해서는 규정하고 있지 않다.(구문 사례 분석을 이용하지 않는 다른 이유는 이것이 장황하고 어색하며, 스펙에 많은 영향을 미치기 때문이다.)
레퍼런스 타입은 함수를 호출할 때 this 값이 결정되는 과정을 설명하는 데도 쓰인다.
레퍼런스는 객체의 프로퍼티를 참조한다. 레퍼런스는 2개의 구성요소를 가지고 있다. base 객체와 property name이다.”

 
 
 
함수 호출과 비-레퍼런스 타입(Function call and non-Reference type)


위에서 봤듯이, 호출 괄호의 왼편에 레퍼런스 타입이 아닌 다른 값이 오는 경우에 this는 자동으로 null 값을 갖게 되고 이 값은 결과적으로 전역 객체가 된다.
이러한 표현식 예를 생각해보자.
 

(function () {
  alert(this); // null => global
})();

 
이 경우에, 레퍼런스 타입의 객체가 아닌 함수 객체(식별자도 아니고, 프로퍼티 접근자도 아니므로 레퍼런스 타입이 존재하지 않는다)를 갖게 되고, 따라서 this 값은 결국 전역 객체로 설정된다.
 
더 복잡한 예제를 보자.
 

var foo = {
  bar: function () {
    alert(this);
  }
};
foo.bar(); // Reference, OK => foo
(foo.bar)(); // Reference, OK => foo
(foo.bar = foo.bar)(); // global?
(false || foo.bar)(); // global?
(foo.bar, foo.bar)(); // global?

 
그렇다면, 왜 어떤 호출에서는 중간 결과 값이 레퍼런스 타입이어야 하는 프로퍼티 접근자의 this 값이 base 객체가 아닌 전역 객체를 갖게 되는 걸까?
문제는 마지막 세 번의 호출은 어떠한 연산이 실행된 이후에 이미 호출 괄호의 왼편에 레퍼런스 타입이 아닌 값을 갖게 된다는 것이다.
첫번째 경우에는 분명한 레퍼런스 타입이 존재하고 따라서 this의 값이 base 객체, 즉 foo 라는 것이 분명하다.
두번째 경우에는 그룹핑 연산자가 레퍼런스 타입의 값으로부터 객체의 실제 값을 얻기 위한 메소드인 GetValue(ECMA-262-3 11.1.6 참고)에 적용되지 않는다. 그래서 그룹핑 연산자가 평가 결과를 반환할 때도 여전히 레퍼런스 타입의 값이 존재 하게 되는데, 이것이 this 값이 다시 base 객체로 설정되는 이유다.
세번째의 경우는, 그룹핑 연산자와 다르게 할당 연산자는 GetValue 메소드(ECMA-262-3 11.13.1의 step 3을 참고)를 호출한다. 반환의 결과로 this 가 null로 설정되었음을 의미하는 함수 객체(레퍼런스 타입 값은 아닌)가 반환되기 때문에, 이는 결국 전역 객체가 된다.
네번째와 다섯번째의 경우도 유사하다. 콤마 연산자와 논리적 OR 표현식은 GetValue 메소드를 호출하고, 따라서 레퍼런스 타입의 값을 잃어버리고 함수 타입의 값을 갖게 되어 this의 값은 전역 객체로 설정된다.
 
 
 
레퍼런스 타입과 값이 null인 this(Reference type and null this value)


호출 괄호의 왼편에 있는 호출 표현식이 호출 괄호의 왼편에서 레퍼런스 타입의 값을 결정할 때 this 값이 null이기 때문에, 결과적으로 this 값이 전역 객체로 설정되는 경우가 있다. 이것은 레퍼런스 타입 값의 base 객체가 활성화 객체인 경우와 관련이 있다.
부모 함수가 자신의 내부 함수를 호출하였을 때 이러한 상황을 볼 수 있다. 두번째 챕터에서 보았듯이 지역 변수, 내부 함수 그리고 형식 매개변수는 주어진 함수의 활성화 객체에 저장이 된다.
 

function foo() {
  function bar() {
    alert(this); // global
  }
  bar(); // AO.bar()와 같다.
}

 
활성화 객체는 항상 this 값으로 null을 반환한다.(즉, 수도 코드인 AO.bar()는 null.bar()와 같다.) 여기에서 다시 위의 설명으로 돌아가보면, this 값에 전역 객체가 설정된다.
with 객체가 함수 이름 프로퍼티를 갖는 경우, with 문의 블럭 안에서 함수를 호출 할 때는 예외일 수 있다. with 문은 자신의 스코프 체인의 가장 앞, 즉 활성화 객체 앞에 그 객체를 추가한다. 따라서 레퍼런스 타입 값을 얻으려 할 때(식별자나 프로퍼티 접근자를 이용해서) 활성화 객체가 아닌 with 문의 객체를 base 객체로 갖게 된다. 그런데, 이는 with 객체가 스코프 체인에서 더 상위에 있는(전역 객체 또는 활성화 객체) 객체까지 가려버리기 때문에 중첩함수 뿐만 아니라 전역 함수와도 관련이 있다.
 

var x = 10;
with ({
  foo: function () {
    alert(this.x);
  },
  x: 20
}) {
  foo(); // 20
}
// because
var  fooReference = {
  base: __withObject,
  propertyName: 'foo'
};

 
catch 절의 실제 파라미터인 함수를 호출할 것도 이와 유사하다. 이 경우에 항상 스코프 체인의 가장 앞, 즉 활성화 객체나 전역 객체 앞에 catch 객체가 추가된다. 그러나 이 동작은 ECMA-262-3의 버그로 인정되어 새로운 버전인 ECMA-262-5에서는 수정 된다. ECMA-262-5는 이러한 경우 this 값이 catch 객체가 아닌 전역 객체로 설정된다.
 

try {
  throw function () {
    alert(this);
  };
} catch (e) {
  e(); // __catchObject - ES3, global - ES5에서는 수정
}
// on idea
var eReference = {
  base: __catchObject,
  propertyName: 'e'
};
// 그러나, 이것은 버그이기 때문에
// this는 강제로 전역 객체를 참조하게 된다.
// null => global
var eReference = {
    base : global,
    propertyName: 'e'
}

이름있는 함수 표현식(named function expression)을 재귀적으로 호출할 때도 상황이 같다(함수에 대한 더 자세한 내용은 Chapter 5. Functions에 있다). 함수를 최초 호출할 때, base 객체는 부모 활성화 객체(또는 전역 객체)이다. 재귀적으로 호출하였을 때 base 객체는 선택적으로 부여한 함수 표현식의 이름을 저장하고 있는 특별한 객체가 된다. 그러나 이러한 경우에 this 값은 항상 전역 객체로 설정된다.
 

(function foo(bar) {
  alert(this);
  !bar && foo(1); // 함수의 이름을 저장하고 있는 특별한 객체이어야 하지만, 항상 전역 객체로 바뀐다.
})(); // global

 
 
 
생성자로 호출된 함수 안의 this(This value in function called as the constructor)


함수 콘텍스트의 this 값과 관련있는 경우가 한 가지 더 있다. 생성자로서 함수를 호출하는 경우이다.
 

function A() {
  alert(this); // 새롭게 만들어진 객체, 아래에서 a 객체
  this.x = 10;
}
var a = new A();
alert(a.x); // 10

 
이 경우는, new 연산자가  A함수의 내부 [[Construct]] 메소드를 호출하고 차례로, 객체가 만들어진 후에 A와 모두 같은 함수인 내부의 [[Call]] 메소드를 호출하여 this 값으로 새롭게 만들어진 객체를 갖게 된다.
 
 
함수 호출시 this를 수동으로 지정하기(Manual setting of this value for a function call)


함수 호출 시에 this 값을 수동적으로 지정할 수 있게 해주는 두 가지 방법이 Function.prototype에 정의되어 있다(prototype에 정의되어 있으므로 모든 함수가 이용 가능). 바로 apply와  call메소드다.
두 메소드 모두 다 호출된 콘텍스트에서 이용할 this 값을 첫번째 인자로 받는다. 이 두 메소드는 별 차이가 없다. apply 메소드는 두 번째 인자로 배열(또는 arguments와 같은 유사배열 객체)이 와야 하고, call 메소드는 어떠한 인자라도 허용한다. 두 메소드의 필수 인수는 this 값으로 이용할 첫번째 인수 뿐이다.
 
예제 :
 

var b = 10;
function a(c) {
  alert(this.b);
  alert(c);
}
a(20); // this === global, this.b == 10, c == 20
a.call({b: 20}, 30); // this === {b: 20}, this.b == 20, c == 30
a.apply({b: 30}, [40]) // this === {b: 30}, this.b == 30, c == 40

 
 
결론(Conclusion)


이 글에서 ECMAScript의 this 키워드의 특징(그리고 이 특징은 C++이나 Java와는 아주 대조적이다.) 에 대해서 이야기를 했다. 이 글이 this 키워드가 ECMAScript에서 어떻게 동작하는지 이해하는 데에 도움이 되었으면 좋겠다. 항상 그랬듯이, 궁금한 점은 댓글로 질문해 준다면 좋겠다.
 
 
추가 문헌(Additional literature)


10.1.7 – This;
11.1.1 – The this keyword;
11.2.2 – The new operator;
11.2.3 – Function calls.
 
 
 


개발왕 김코딩

Howdy. Why so serious?

0개의 댓글

답글 남기기

아바타 플레이스홀더

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다