자바스크립트에서 this는 무엇일까요? OOP에 익숙한 분들은 클래스에서 this 키워드는 클래스로 만들어진 객체 자신을 가리킨다고 알고 있습니다. 또한 상속구조라면 super키워드는 자식의 부모를 가리키죠.
자바스크립트의 this 본질을 알기 위해서는 먼저 컨텍스트(context)에 대한 개념을 알아야 합니다. 컨텍스트는 자바스크립트 코드가 실행될 때, 인터프리터가 new 키워드 또는 리터럴 표기법(new Array:[], new Object:{}, new RegExp: // 자바스크립트에서는 리터럴 표기법이 딱 3가지만 존재)을 만나면 heap 메모리에 만들어지는 별도의 공간입니다. 그러므로 자바스크립트의 모든 함수를 생성자로 삼아 new를 사용하면 컨텍스트가 만들어지게 되는 것입니다.
자바스크립트에서 new 키워드는 오직 함수에만 붙일 수 있습니다.
f = function() {
};
instance = new f();
저런식으로 만들어진 함수일 때 new를 할 수 있습니다. new를 통해 만들어진 heap 메모리 공간인 컨텍스트는 instance에 참조가 되겠지요.
재미있는 것은 new Object도 됩니다. 즉 Object도 함수라는 소리입니다. Array, RegExp, Number, String, Boolean 사용자 정의가 아닌 자바스크립트 기본 네이티브 객체들도 모두 함수입니다. 실제로 typeof 해보시면 'function'이라고 나옵니다. 그리고 new를 통해 만들어진 인스턴스를 typeof해보면 'object'라고 나오지요. 자바스크립트 인터프리터는 자바스크립트 코드를 실행하기 시작할 때 window 객체를 만들고 이 객체의 key-value에 저 네이티브 객체들을 생성해 값으로 참조시킵니다. 다음과 같이요.
window.Object = function () {};
window.Array = function () {};
window.RegExp = function () {};
window.Number = function () {};
window.String = function () {}
window.Boolean = function () {};
실제로 new Object는 new window.Object와 같습니다.
사용자 정의든 인터프리터 자체 정의된 함수이든지 아직 컨텍스트라고 부르지 않습니다. 자바스크립트는 이렇게 함수 정의를 만나 해석하면 함수 객체의 스코프 메모리 공간을 따로 만들고 호출 또는 생성에 관여할 준비를 시키는 것입니다. 이때 호출은 단순히 정의된 함수를 f()식으로 호출하는 행위입니다.(정확히는 적용한다라고 합니다.) 생성은 new 키워드를 사용해 컨텍스트를 만들지요.
이렇게만 보면 자바스크립트는 딱 2개의 동적 생성 메모리 공간만 있습니다.
* Object 객체의 key-value 공간
* 함수의 스코프(scope;유효범위) 공간
결과적으로 Object 객체의 key-value 공간이 바로 컨텍스트이고 함수의 스코프 공간을 사용해 new연산자로 새로운 Object 객체의 key-value공간을 만들 수 있는 셈입니다.
그럼 this를 보지요. this는 컨텍스트를 가리키되 실행 컨텍스트를 가리킨다고 했습니다. 단순하게 이야기해 new 연산자를 통해 만들어진 Object 객체의 key-value 공간을 가리키는 것이고 이렇게 가리키게 되었을때 컨텍스트가 this에 바인딩되었다고 말합니다. 이때 컨텍스트를 실행 컨텍스트(excution context)라고 하고요. 그럼 자바스크립트는 어떤 인터페이스로 바인딩을 시킬까요? 바로 함수를 호출할때 입니다.
k = 3;
f = function(a, b) {
return this.k + a + b;
};
f(3, 4);
위 코드는 자바스크립트 인터프리터 입장에서 바라보면 다음과 같습니다.
window['k'] = 3;
window['f'] = function(a , b) {
return this.k + a + b;
};
window['f'].apply(window. {'0':3, '1':4, length:2}); //10
f는 분명히 함수입니다. 함수 객체인 Function의 프로토타입에는 apply()라는 특별한 함수가 내장되어 있습니다.(그렇다기 보다 인터프리터가 function일때 apply를 만나면 구동해주는 것 뿐이겠죠.) 함수를 f(3,4) 처럼 호출한다는 것은 실제로는 f.apply( window, [3, 4] ) 처럼 적용하는 것으로 해석됩니다. 그래서 자바스크립트에서 함수 호출은 맞지 않고 함수 적용이라는 말이 더 어울립니다. 아무튼 더 살펴보면 이 apply() 함수의 첫번째 인자는 컨텍스트로 삼을 인스턴스를 넘겨줍니다. 두번째 인자는 실제 함수에 넘겨주는 인자입니다. 여기서 중요한 것은 첫번째 인자인데 이렇게 컨텍스트를 함수를 통해 인자로 넘겨주면 이 컨텍스트가 실행 컨텍스트로 활성화 되고 함수 실행 내부에서 this는 이 실행 컨텍스트를 가리키게 됩니다. 결국 this.k는 window.k이고 이 값은 3이기 때문에 결과가 10이 되는 겁니다.
중요한 것은 위 코드처럼 window를 직접 쓰지않고 암시적으로 key-value로 잡으면 바로 window에 예속됩니다.
k = 3;
o = {
k: 10,
f : function(a, b) {
return this.k + a + b;
}
};
o.f(3, 4); //17
위 코드를 자바스크립트 인터프리터 입장에서 재해석해보면 다음과 같습니다.
window.k = 3;
window.o = new Object();
window.o.k = 10;
window.o.f = function(a, b) {
return this.k + a + b;
};
window.o.f.apply( o, [3, 4] ); //17
좀 전에 본 것과 다른 것은 window가 아닌 새로 생성된 인스턴스인 o는 new 연산자를 통해 컨텍스트가 만들어졌고 여기에 key-value 맵이 생성되어 각각 k=10, f=함수가 추가되었습니다. 앞선 코드에서 window의 key-value로 잡힌 것과 다르다는 점을 확인하세요. 이 코드는 명시적으로 새로운 컨텍스트를 만들어 이곳에 key-value를 잡아 이 컨텍스트에 예속된 셈입니다. 그리고 마지막에 실행 컨텍스트로 o를 apply를 통해 첫번째 인자로 넘겨지게 된 것입니다. 이때 함수내 this는 바로 o가 되고 this.k는 window.k가 아닌 o.k가 됩니다. 결국 결과는 17이 되지요.
여기서 점(.) 구문과 함수이름()이 조합된 "인스턴스명.함수이름();" 를 자바스크립트 인터프리터가 만나면 "함수이름.apply(인스턴스명)" 으로 인지합니다. 이렇게 해서 함수 내부에 this는 apply 함수의 첫번째 인자로 넘긴 인스턴스명에 해당하는 컨텍스트가 바인딩되는 것입니다. 그러한 이유로 만약 함수에 this를 쓰지 않으면 이것은 절대로 컨텍스트 o의 key-value 시스템에 잡혀 있는 k를 탐색하지 않고 그 상위 객체인 window.k를 찾게 됩니다.(이 탐색부분은 별도로 설명할 예정입니다. 더 복잡하다는 ^^) 중요한 것은 컨텍스트에 있는 k를 참조하고 싶으면 반드시 this를 쓰세요.
이렇게 보면 window도 결국 함수로 만들어진 또 하나의 인스턴스이고 별도의 컨텍스트가 있다는 말이 됩니다. 이것은 자바스크립트 인터프리터가 자바스크립트 코드를 실행하면서 사용자가 직접 new하지 않고 만들어지는 유일한 컨텍스트입니다. 그래서 정리하면 window가 기본 실행 컨텍스트가 되는 이유는 다음처럼 정리됩니다.
(function window(){
//<script>로 감싼 모든 사용자 자바스크립트 코드는 여기 안에 있는 셈!
}).apply(window);
위 코드는 약간 반칙성 같지만 자바스크립트 인터프리터가 저렇게 한다는 것을 설명해주기에는 가장 적당합니다. 중요한 것은 마지막 apply()의 첫번째 인자에 window를 실행 컨텍스트로 넘긴다는 것입니다. window는 인스턴스인 동시에 함수인 셈이죠.(window 만 예외적으로 그렇습니다.) 그러므로 컨텍스트를 넘기지 않고 .apply(null)하면 자동으로 window를 실행 컨텍스트로 만들어주고 이 때 this는 window로 바인딩 되지요.
자바스크립트는 매우 동적인 언어입니다. 그래서 저 실행 컨텍스트도 자기 마음대로 설정할 수 있습니다.
o.f.apply(window, [3, 4]); //10
위처럼 호출하면 실행 컨텍스트는 window로 잡히고 함수 내부에 this.k는 window.k가 되기 때문에 결과는 10이 되지요.
지금까지는 실행 컨텍스트와 this 바인딩을 이해하는 것에 무리가 없습니다. 조금더 나아가 봅시다.
function foo(){
function bar(){
console.log(this); //DOMWindow
}
console.log(this); //DOMWindow
bar();
}
foo();
결과는 무엇일까요? 네 this는 모두 window입니다. 함수가 외부, 내부로 중첩되어 있는 상황이고 foo()함수를 호출하면 foo.apply(window); 한 것과 같기 때문에 foo()함수 실행시 컨텍스트는 window입니다. 그 상태에서 bar()함수를 호출했다는 것은 현재 실행 컨텍스트가 window이므로 bar.apply(window); 입니다. 그러므로 bar() 함수의 this는 바로 window입니다.
위 코드를 조금 복잡하게 만들어 보겠습니다.
function foo(){
function bar(){
console.log('2:', this);
}
console.log('1:', this);
b = new bar();
console.log('3:', this.b);
console.log('4:', window.b);
console.log('5:', b);
}
foo();
console.log('6:', b);
결과는 아래와 같습니다.
1: DOMWindow
2: bar
3: bar
4: bar
5: bar
6: bar
찬찬히 살펴보지요. foo()함수를 호출합니다. 그래서 만나는 첫번째 콘솔('1:' 부분)에서 this는 window라는 것은 이제 잘 아실겁니다. 그런데 다음이 중요합니다. new bar()는 bar 함수를 생성자로 쓴다는 의미입니다. 생성자도 결국 함수를 호출하는 행위를 한다는 것이 여기서 볼 수 있습니다. 실제로 b = new bar();는 다음과 같은 일을 합니다. (다음처럼 바뀐다가 아니라 개념상 그렇다는 겁니다.)
b = new; //해쉬맵 생성
b.constructor = bar; //constructor키에 함수 객체를 참조. b생성을 어떤 것으로 했는가 척도가 됨
b.__proto__ = b.construrctor.prototype; //bar 객체에 만들어진 프로토타입을 __proto__키의 값으로 참조
b.constructor.apply( b, arguments ); //함수 실행
지금까지 설명한 부분은 아니지만 어쨌든 new 키워드는 컨텍스트를 생성하고 각종 속성을 할당한 다음 마지막에 apply()를 호출해주는 역할까지 합니다. 이 관점에서 바라본다면 apply()의 첫번째 인자가 바로 b가 되므로 bar의 인스턴스가 실행 컨텍스트임을 명시해줍니다. 그러므로 bar()함수가 실행되면서 그 내부에 this는 바로 bar의 인스턴스가 실행 컨텍스트로 잡힙니다. 함수가 종료되면 그전 실행 컨텍스트인 window로 돌아가겠죠. 그 다음 콘솔('3:'~'5:' 부분)은 b가 window의 key-value로 잡혔음을 알려줍니다. this.b = new bar();로 했다고 하더라도 결과는 달라지지 않을겁니다. 왜냐하면 현재 실행 컨텍스트는 window일 테니깐요. 대신 var b = new bar()를 했다면 3, 4번째는 undefined가 나올 것입니다. 실행 컨텍스트인 window의 key-value가 아닌 함수 foo()의 스코프에 b가 정의될테니깐요. 대신 5번째의 경우에는 bar가 나오겠죠. foo() 함수의 스코프 b가 될테니깐요. 위 코드 상에서 미지막 6번째가 bar가 되는 것은 foo() 함수에서 실행컨텍스트인 window에 b를 키로 잡고 new bar()을 통해 만들어진 인스턴스를 값으로 잡았기 때문입니다.
다른 예를 들어보겠습니다.
function foo(){
this.bar = function(){
console.log('2:', this);
}
console.log('1:', this);
this.bar();
}
f = new foo();
f.bar();
결과는 아래와 같습니다.
1: foo
2: foo
2: foo
우리는 foo()함수를 직접 호출한게 아니라 생성자로 사용했습니다. 그래서 앞서 설명한대로 하자면 f = new foo()는 다음과 같은 동작을 합니다.
f = new; //해쉬맵 생성
f.constructor = foo; //constructor키에 함수 객체를 참조. f생성을 어떤 것으로 했는가 척도가 됨
f.__proto__ = f.construrctor.prototype; //foo 객체에 만들어진 프로토타입을 __proto__키의 값으로 참조
f.constructor.apply( f, arguments ); //함수 실행
이렇게 되니 결국 foo의 인스턴스 생성시 실행 컨텍스트는 f가 되어 this는 'foo'로 출력됩니다. 다음으로 this.bar()를 호출하는데 이미 this는 f을 컨텍스트로 삼고 있으므로 bar 함수내에 this도 f.bar.apply(f)와 동일하게 되어 'foo'를 출력합니다. 마지막에 f.bar()는 f의 key-value에 선언된 bar 함수를 호출하므로 결국 'foo'를 출력합니다.
정리
이제 우리는 알아야 합니다. this는 함수 적용시 넘겨주는 실행 컨텍스트로 삼을 컨텍스트를 가리키는 것을 의미하는 것이고 이런 행위가 함수 호출이 아니라 적용이기 때문에 일반적인 언어의 함수 호출과는 다른 의미라는 것을 말입니다. new 키워드를 사용해 함수로부터 컨텍스트를 만들 수 있다는 것도 알 수 있게 되었습니다. 또한 window는 시스템이 new가 없이 자동으로 만들어주는 유일한 컨텍스트이다라는 점도요. 그래서 기본 실행 컨텍스트는 window가 될 수 밖에 없다는 것도 우리는 알게 된 것입니다. 이쯤 되면 많은 책들에서 이야기 하는 컨텍스트니 this니 함수 호출이니라는 용어 설명이 너무나도 다른 언어의 추상적 관점에서 바라보고 설명했다는 것을 알게 됩니다. 그래서 자바스크립트가 배우기 어렵고 최적화가 힘든 것이겠지요. 저도 힘듭니다. ㅎㅎ
참고
- 제 블로그내 자바스크립트 글 : http://blog.jidolstar.com/category/%EA%B0%9C%EB%B0%9C/JavaScript
- JavaScript Zero : http://zero.diebuster.com/zero
- JavaScript Core : http://frends.kr/topics/javascript-core/
- ECMA : http://frends.kr/topics/ecma-262-3rd-%EC%9E%90%EC%84%B8%ED%9E%88-%EB%B3%B4%EA%B8%B0-chapter-1-execution-contexts/
- JavaScript - this : http://dmitrysoshnikov.com/ecmascript/chapter-3-this/
글쓴이 : 지돌스타(http://blog.jidolstar.com/810)
'JavaScript' 카테고리의 다른 글
[jQuery] Ajax 방법 (0) | 2014.05.08 |
---|---|
jQuery.ajax() (0) | 2013.06.25 |
자바스크립트의 typeof 연산자에 대해 (0) | 2013.05.09 |
자바스크립트 코딩기법과 핵심패턴 제 6장 코드 재사용 패턴 #2 (0) | 2013.05.09 |
자바스크립트 코딩기법과 핵심패턴 제 6장 코드 재사용 패턴 #1 (0) | 2013.05.09 |