Javascript 에 관련된 글 중 가장 인상 깊었던 글을 번역해 보려 한다. 중요 용어(object 등)는 영어를 그대로 사용하려 한다 (비록 괄호 안에 해석을 넣긴하겠지만). 원문에서는 각 섹션마다 관련이 있는 글에 대한 링크가 있으나 이 번역에서는 그 링크를 담지 않는다.
변역해보고 나서 알게 된 사실이 이미 다른 분이 각 섹션에 링크되어 있는 detail을 번역해 놓으셨다. http://huns.me/?s=ECMA&x=0&y=0 에 보면 각 섹션의 detail 내용을 볼수 있다.
이 글을 ECMA-262-3 in detail 시리즈에 대한 개요와 서머리에 관한 것이다. 각 섹션은 거기에 해당하는 챕터를 레퍼런스로 담고 있기 때문에 더욱 자세한 내용을 알고 싶다면 관련 글을 읽어보길 바란다.
이글의 대상 독자 : 숙련된 프로그래머
우리는 ECMAscript의 기초가 되는 object의 개념에서 부터 시작하도록 한다.
An object
ECMAscript 는 객체지향 언어로서 object(객체)를 다룬다. 물론 primitives (기본자료형) 도 있지만 필요에 따라 이 역시 object로 변환된다.
object란 property(속성)의 집합으로 하나의 prototype object(프로토타입 객체) 를 가지고 있다. phototype은 object 또는 null 값이다.
object의 기본적인 예를 봐보자. 객체가 가지고 있는 prototype은 [[Prototype]] 이라는 속성으로 가르키게 된다. 그러나 그림에서 우리는 __<내부속성>__ 처럼 괄호 대신 밑줄을 이용하도록 하겠다. 특히 prototype object를 가르키는 속성으로 __proto__를 사용하도록 하겠다.
코드를 보면
var foo = {
x: 10,
y: 20};
x: 10,
y: 20};
위 코드의 foo라는 object는 두개의 속성(property) 와 prototype을 가르키는 내부적인 속성인 __proto__ 속성을 가지고 있다.
무엇을 위해 이 prototype이 필요한 것일까? 이 질문에 답하기 위하여 prototype chain (프로토타입 체인) 이라는 개념에 대해 알아보자
A prototype chain
prototype obejct는 단순히 object 이고 그렇기 때문에 그것 또한 자신만의 property(속성)을 가지고 있다. 만약에 prototype의 __proto__ 속성이 null값이 아닌 또 다른 prototype을 가리키고 있다면 이것을 prototype chain(프로토타입 체인) 이라 한다.
prototype chain은 객체들의 유한한 사슬(chain)으로서 상속(inheritance)와 속성의 공유(shared property)를 구현하는데 사용되어 진다.
거의 모든 부분이 유사하고 약간의 부분만 다른 두 object(객체) 가 있다고 할때, 좋은 디자인은 이미 하나의 object에 구현되어 있는 코드를 재사용하는 것이다. class(클래스) 기반의 언어에서는 이와같은 코드의 재활용 문법을 class-based inheritance(클래스 기반 상속) 이라고 한다. 공통되는 기능을 class A에 구현하고 class B, C가 A로 부터 상속을 받아 약간의 추가적인 코드만 넣는 것이다.
ECMAScript 에서는 class의 개념이 없다. 그러나 코드 재사용 문법은 별반 다르지 않으며(심지어 어떤면에서는 더욱 유연하다) prototype chain 을 이용한다. 이와 같은 상속을 delegation based inheritance 또는 prototype-based inheritance 라 한다.
class A, B, C에 비슷하게 ECMAscript는 a, b, c 라는 object를 생성한다. object a가 공통된 부분을 가지고 있고 b, c가 각각의 다른 부분(속성이나 메소드)를 갖는다.
var a = {
x: 10,
calculate: function (z) {
return this.x + this.y + z;
}};
var b = {
y: 20,
__proto__: a
};
var c = {
y: 30,
__proto__: a
};
// call the inherited method
b.calculate(30); // 60
x: 10,
calculate: function (z) {
return this.x + this.y + z;
}};
var b = {
y: 20,
__proto__: a
};
var c = {
y: 30,
__proto__: a
};
// call the inherited method
b.calculate(30); // 60
c.calculate(40); // 80
쉽지 않는가? 우리는 object b, c가 a에 정의되어 있는 속성에 접근하여 이용하는 것을 볼 수 있다.
규칙은 단순하다. 만약에 특정 속성이 object 그 자신에게서 발견이 되지 않는다면 prototype chain에 있는 object의 속성을 확인하는 것이다. 만약 prototype의 속성에도 특정 속성이 없다면 prototype의 prototype의 속성을 확인하는 식으로 계속 속성을 찾게 된다. prototype chain에서 처음으로 찾아지는 속성을 사용하게 된다. prototype chain 에 의해 연결된 어떠한 prototype에도 찾고자 하는 특정 속성이 없으면 undefined 값을 리턴하게 된다.
주목해야 할것은 this라는 키워드는 현재 사용되어 지는 object를 가르키게 된다. a object에 있는 this 는 b.calculate에서 b object가 현재 사용되어 지는 object이기 때문에 b를 가르키게 된다. this.x 에서의 this 는 b에서 x라는 property 찾을 수 없기 때문에 prototype chain에 의해 a에 있는 x 속성을 가르키게 된다.
만약에 object의 prototype이 명시 되어 지지 않았다면 __proto__라는 속성은 Object.prototype을 가르키게 되며 Object.prototype의 __proto__ 속성은 null 값을 가르키게 되고 이는 prototype chain의 마지막 링크가 된다.
아래 그림은 위 코드의 a, b, c object간의 상속 관계를 도식화 한 것이다.
ES5 에서는 아래와 같은 방법으로 prototype-based inheritance를 구현할 수 있다.
var b = Object.create(a, {y: {value: 20}});
var c = Object.create(a, {y: {value: 30}});
때때로 갖은 property 구조를 갖지만 그 값은 다른 object를 만들 필요가 있을 때가 있다. 이와 같은 경우 constructor function (생성자 함수) 를 사용할 수 있다.
Constructor
object 생성에 있어 constructor 함수는 또다른 유용한 방법이다. constructor함수를 이용해서 object 를 생성하게 되면 새로이 생성되는 object의 prototype object를 자동으로 설정할 수 있게 된다. constructor 함수에 의해 생성되는 object가 가르키게 될 prototype object 는 constructor 함수의 prototype 속성에 연결되어 있다 (주의 할것은 여태 prototype이라는 용어를 object를 의미하는 것으로 사용하였는데 constructor 함수의 prototype 속성은 말그대로 속성의 key 값을 의미한다).
prototype chain 섹션의 코드를 아래와 같이 constructor 함수를 이용한 방법으로 변환할 수 있다(object a의 역할을 여기서는 Foo라는 constructor 함수가 대신하게 된다).
// a constructor functionfunction Foo(y) {
// 이것이 constructor 함수 이다.
// constructor 함수를 이용해 object를 생성시 y 값을 받아서
// 생성된 object 자신만의 y값을 갖게 된다.this.y = y;}
// also "Foo.prototype" stores reference
// to the prototype of newly created objects,
// so we may use it to define shared/inherited
// properties or methods, so the same as in
// previous example we have:
// inherited property "x"
Foo.prototype.x = 10;
// and inherited method "calculate"
Foo.prototype.calculate = function (z) {
return this.x + this.y + z;};
// now create our "b" and "c"// objects using "pattern" Foovar b = new Foo(20);var c = new Foo(30);
// call the inherited method
b.calculate(30); // 60
c.calculate(40); // 80
// let's show that we reference// properties we expect
console.log(
b.__proto__ === Foo.prototype, // true
c.__proto__ === Foo.prototype, // true
// also "Foo.prototype" automatically creates
// a special property "constructor", which is a
// reference to the constructor function itself;
// instances "b" and "c" may found it via
// delegation and use to check their constructor
b.constructor === Foo, // true
c.constructor === Foo, // true
Foo.prototype.constructor === Foo, // true
b.calculate === b.__proto__.calculate, // true
b.__proto__.calculate === Foo.prototype.calculate // true
);
위 코드는 아래와 같이 도식화 될 수 있다.
위 그림에서와 같이 constructor 함수에 의해 생성된 모든 object는 prototype object를 갖게 된다. Foo constructor 함수도 함수 이기 때문에 javascript에서는 함수도 객체이기에 __proto__ 속성을 가지고 있고 이는 Function.prototype 이라는 object를 가르키게 된다. 이 Function.prototype의 __proto__ 속성은 Object.prototype을 가르키기 된다. Foo의 prototype 속성은 object를 생성하는 constructor로서의 역할을 수행할때 생성되는 object의 prototype이 될 object를 가르키는 속성에 지나지 않는다.
ES6 에서는 class 의 개념을 표준화 하여 constructor 함수 위에 class 라는 syntactic sugar를 만들었다.
// ES6class Foo {
constructor(name) {
this._name = name;
}
getName() {
return this._name;
}}
class Bar extends Foo {
getName() {
return super.getName() + ' Doe';
}}
var bar = new Bar('John');
console.log(bar.getName()); // John Doe
constructor(name) {
this._name = name;
}
getName() {
return this._name;
}}
class Bar extends Foo {
getName() {
return super.getName() + ' Doe';
}}
var bar = new Bar('John');
console.log(bar.getName()); // John Doe
이제 object의 특성을 바탕으로 ECMAScript가 어떻게 runtime program execution을 구현하는지 알아보자. 이것은 execution context stack(실행 문맥 스택)으로 불리는데 이것의 모든 요소들이 (ECMAScript가 항상 그렇듯) 추상적으로 object로 표현되어 진다.
Execution context stack
ECMAScript 코드에는 3가지 종류의 코드가 있다: global code, function code, eval code. 모든 코드는 execution context(실행 문맥) 안에서 평가되어 진다. global context(문맥)는 오직 하나만 있고 그 안에 많은 함수의 instance와 eval execution context(실행 문맥)들이 있다. 매번 함수가 호출될 때마다 함수 실행 문맥으로 들어가게 되고 함수 코드 타입을 평가하게 된다. 마찬가지로 eval 함수가 호출되면 eval 실행 문맥으로 들어가고 그것의 코드가 평가된다.
함수가 호출 될때마다 문맥이 생성되기 때문에 하나의 함수는 수많은 문맥을 생성할 수 있으며 아래 코드와 같이 서로 다른 문맥이 서로 다른 context state(문맥 상태)를 가지고 만들어 진다.
function foo(bar) {}
// call the same function,// generate three different// contexts in each call, with// different context state (e.g. value// of the "bar" argument)
foo(10);
foo(20);
foo(30);
// call the same function,// generate three different// contexts in each call, with// different context state (e.g. value// of the "bar" argument)
foo(10);
foo(20);
foo(30);
곧 함수가 또 다른 함수를 호출할때와 같이 실행 문맥은 또다른 실행 문맥을 활성화 시킬수 있다. 이러한 상황은 stack(스택)으로 구현되어 있으며 이를 execution context stack (실행 문맥 스택) 이라 한다.
문맥을 활성화 시킨 문맥을 caller (호출자) 라 한다. 반면에 활성화 된 문맥은 callee (피호출자) 라 한다. 피호출자가 또 다른 문맥을 활성화 시키게 된다면(다른 함수를 호출하게 된다면) 이는 또 호출자가 된다.
호출자가 피호출자를 활성화 시키면(한 함수가 다른 함수를 호출하면) 호출자는 자신의 실행을 중단하고 제어권을 피호출자에게 넘겨준다. 이는 피호출자가 스택에 쌓이게 되는 것이고 이 피호출자의 실행문맥이 활성화 되는 것이다(active execution context). 피호출자의 문맥이 끝나게 되면 제어권이 다시 호출자에게 넘어가고 나머지 문맥이 평가된다(혹은 실행된다). 피호출자는 단순히 return을 하거나 exception과 함께 끝나면서 스택에서 빠지게 된다.
곧 모든 ECMAscript 의 program runtime (프로그램 런타임)은 아래 그림과 같이 실행문맥 스택 (execution context stack = EC stack)으로 표현되며 이 스택에서 젤 위에 있는 문맥이 활성화 상태의 문맥 (active context) 가 된다.
위 그림을 자세히 설명하면, 프로그램이 실행될때 global EC 에 들어가게 되고 이는 스택에 맨 처음 쌓이게 된다. 그러면 global code에 따라 일부 초기화 (필요한 객체와 함수들이 만들어 지는것)가 이루어 진다. global 문맥이 실행되는 동안 global code가 다른 함수를 호출(활성화)하면 그 함수의 EC로 들어가게 되고 이 EC는 스택에 쌓이게 된다. 또는 초기화된 이후 runtime system(런타임 시스템)은 특정 이벤트가 발생하기를 기다리고, 특정 이벤트가 발생하면 이벤트에 관련되 함수가 호출되고(활성화) 그 함수의 EC로 들어가게 되며 이 함수의 EC가 스택에 쌓이는 것이다.
아래는 프로그램이 시작될때 global EC가 스택에 쌓이고, global code에서 특정 함수를 호출하여 특정함수의 EC가 스택에 추가 된 이후, 함수가 실행 완료되어 스택에서 없어진 프로세스를 나타내는 그림이다.
이것이 바로 ECMAScript의 runtime system이 어떻게 코드의 실행을 다루는가를 보여준다.
위에서 말했듯 스택에 있는 모든 실행 문맥은 객체로 나타내어 진다. 이제 실행문맥을 나타내는 객체의 구조와 property(속성)에 대해 알아보자
Execution context
execution context(실행 문맥)은 객체로 표현되어 진다. 모든 실행문맥은 context's state (문맥 상태) 라 불리는 속성(property)를 갖고 있다. 이 속성들을 통해 연관된 코드의 실행을 추적하게 된다. 아래는 문맥의 구조를 나타낸 그림이다.
실행문맥은 위의 3개의 속성(variable object, this, scope chain) 외에도 구현에 따라 추가적인 속성을 갖을 수 있다.
이 속성들에 대해 자세하게 알아보자
Variable object
variable object (변수 객체)는 실행문맥과 관련이 있는 데이터를 담는 그릇이다. 이것은 문맥에 정의된 variable(변수), function(함수)를 저장하고 있는 객체이다.
주의해야 할것은 함수 표현식 (함수 선언 말고) 은 변수 객체에 포함되지 않는다.
변수 객체는 추상적인 개념이다. 다른 문맥에서는 다른 객체로 표현된다. 예를 들면 global 문맥에서는 global object(객체) 자신이 변수 객체이다(그렇기 때문에 global 변수를 global object를 통해서 직접적으로 접근이 가능한 것이다).
아래 global 실행문맥을 살펴보자.
var foo = 10;
function bar() {} // function declaration, FD(function baz() {}); // function expression, FE
console.log(
this.foo == foo, // true
window.bar == bar // true);
console.log(baz); // ReferenceError, "baz" is not defined
function bar() {} // function declaration, FD(function baz() {}); // function expression, FE
console.log(
this.foo == foo, // true
window.bar == bar // true);
console.log(baz); // ReferenceError, "baz" is not defined
위 코드의 global 문맥의 변수객체(variable object) 는 아래와 같은 속성을 갖는 객체이다.
그림에서와 같이 baz 함수 표현식은 변수 객체에 포함되지 않는다. 이 때문에 baz 함수 자신 이외의 곳에서 baz를 호출하면 ReferenceError가 발생하게 되는 것이다.
주의해야 할 것은 다른 언어와는 달리 ECMAScript는 오직 function(함수) 만이 scope(스코프)를 만든다. 특정 함수 안에서 생긴 변수나 또 다른 함수는 그 함수 밖에서는 보이지 않는다.
eval 역시 새로운 실행 문맥을 생성하나 자신만의 변수 객체를 생성하지 않고 global 변수 객체를 이용한다던지 혹은 eval을 호출한 호출자 함수의 변수 객체를 이용한다.
그러면 함수에서는 변수 객체는 무엇인가. 함수 문맥에서는 변수 객체가 activation object (활성 객체)로 표현되어 진다.
Activation object
함수가 다른 함수 혹은 global 문맥에서 호출되어 활성화 되면 변수 객체 역할을 하는 특별한 객체인 activation object (활성 객체)가 만들어 진다. 일단 활성객체는 함수가 받는 parameter(인자)와 특별한 객체인 arguments (인자들을 array 형태로 갖는 객체) 를 속성으로 갖게 된다. 그 뒤 이 활성객체는 변수 객체로 사용되어 진다. 즉 활성 객체는 인자, aruments를 추가적으로 포함한 변수 객체인 것이다.
아래 예시 코드를 보자
function foo(x, y) {
var z = 30;
function bar() {} // FD
(function baz() {}); // FE}
var z = 30;
function bar() {} // FD
(function baz() {}); // FE}
foo(10, 20);
위코드 함수의 활성 객체는 아래 그림과 같다.
다시 한번 알수 있드시 함수 표현식을 활성 객체에 포함되지 않는다.
다음 색션에서는 scope chain(스코프 체인)에 대해 알아본다. ECMAScript 에서는 함수안에 또 다른 함수인 inner function (내부함수)를 사용할때가 있으며 이 내부함수에서 parent function (부모함수)의 변수에 접근할 수 있다. variable object(변수 객체)를 문맥의 scope object(스코프 객체)라 명명하였듯이 앞서 알아본 prototype chain 또한 소위 scope chain 이라 불린다.
Scope chain
scope chain 은 문맥 안의 코드에 나타난 identifier(식별자)를 찾기 위한 객체들의 리스트이다.
scope chain(스코프 체인) 에서의 규칙은 prototype chain 에서의 규칙과 비슷하다. 특정 변수가 자신의 scope (변수 객체 혹은 활성 객체)에서 발견이 되지 않으면 부모의 변수 객체에서 찾는 것이다.
문맥에 따라 identifier(식별자)는 변수의 이름, 선언된 함수명, 인자 등이 될 수 있다. 문맥의 변수 객체에서 찾을 수 없는 식별자를 free varible이라고 하는데 이 free variable을 찾고자 할 때 scope chain이 사용되어진다.
일반적인 경우 scope chain은 부모의 변수 객체와 자신의 변수 객체(부모의 변수 객체보다 앞에 위치함)의 리스트를 의미한다. 그러나 문맥의 실행 동안 다른 객체(with-objects 또는 catch-clauses의 특별한 객체)가 scope chain에 추가될 수 있다.
식별자를 찾을 때(look up) prototype chain의 방식과 유사하게 활성 객체부터 시작하여(함수의 경우) 상위의 부모 함수 변수 객체를 찾아보게 된다.
var x = 10;
(function foo() {
var y = 20;
(function bar() {
var z = 30;
// "x" and "y" are "free variables"
// and are found in the next (after
// bar's activation object) object
// of the bar's scope chain
console.log(x + y + z);
})();})();
(function foo() {
var y = 20;
(function bar() {
var z = 30;
// "x" and "y" are "free variables"
// and are found in the next (after
// bar's activation object) object
// of the bar's scope chain
console.log(x + y + z);
})();})();
scope chain 의 연결이 활성 객체의 __parent__ 속성에 의해 연결되어 있다고 여긴다. 이 __parent__ 속성이 부모의 활성 객체 (혹은 변수 객체)를 가리키게 된다. 이와 같은 방법이 ES5 lexical environments 에 사용된 방식(outer link라 칭한다)이다. scope chain를 나타내는 다른 방식은 단순히 참조가 되는 활성 객체를 순서대로 array에 담는 것이다. __parent__ 속성을 이용한 구현 방식을 이용하여 우리는 위의 코드를 아래와 같이 표현할 수 있다(부모의 활성 객체는 함수의 [[Scope]] 속성을 저장되어 있다. 주의해야 할 것은 __parent__ 속성은 활성 객체의 속성이고, [[Scope]]는 함수 자체의 속성이다).
코드의 실행 중에 with문과 catch 절 객체 (clause object)에 의해 scope chain의 객체가 추가 될수 있다. 그리고 추가되는 객체가 단순히 객체이기 때문에 prototype을 가지고 있을 수 있다(prototype chain). 이같은 사실로 인해 scope chain을 통한 identifier(식별자) 검색이 two-dimensional (이차원) 적으로 이뤄질 수도 있다. (1) 먼저 scope chain 에 의한 링크가 고려가 되어 자신의 활성 객체 부터 부모들의 활성 객체까지 하나씩 깊이가 깊어지면서 (2) 그 단계에 있는 활성 객체가 가지고 있는 prototype chain에 연결되어 있는 모든 객체를 검사하게 되는 것이다.
Object.prototype.x = 10;
var w = 20;var y = 30;
// in SpiderMonkey global object// i.e. variable object of the global// context inherits from "Object.prototype",// so we may refer "not defined global// variable x", which is found in// the prototype chain
console.log(x); // 10
(function foo() {
// "foo" local variables
var w = 40;
var x = 100;
// "x" is found in the
// "Object.prototype", because
// {z: 50} inherits from it
with ({z: 50}) {
console.log(w, x, y , z); // 40, 10, 30, 50
}
// after "with" object is removed
// from the scope chain, "x" is
// again found in the AO of "foo" context;
// variable "w" is also local
console.log(x, w); // 100, 40
// and that's how we may refer
// shadowed global "w" variable in
// the browser host environment
console.log(window.w); // 20
})();
var w = 20;var y = 30;
// in SpiderMonkey global object// i.e. variable object of the global// context inherits from "Object.prototype",// so we may refer "not defined global// variable x", which is found in// the prototype chain
console.log(x); // 10
(function foo() {
// "foo" local variables
var w = 40;
var x = 100;
// "x" is found in the
// "Object.prototype", because
// {z: 50} inherits from it
with ({z: 50}) {
console.log(w, x, y , z); // 40, 10, 30, 50
}
// after "with" object is removed
// from the scope chain, "x" is
// again found in the AO of "foo" context;
// variable "w" is also local
console.log(x, w); // 100, 40
// and that's how we may refer
// shadowed global "w" variable in
// the browser host environment
console.log(window.w); // 20
})();
위 코드는 아래와 같은 구조를 갖게 된다(__parent__ 속성에 의한 링크를 검색하기 전에 __proto__에 의한 체인을 먼저 검색한다.).
주의해야 할 것은 모든 ECMAScript들의 구현에서 꼭 global 객체가 Object.prototype을 상속받는 것은 아니다라는 것이다(특정 구현에서는 위의 구조화가 틀릴 수도 있다).
부모의 변수 객체가 존재할 때까지 inner function (내부 함수)에서 부모의 데이터를 찾는 것은 전혀 특별한 일이 아니다. 단순히 scope chain 을 통해서 필요한 변수가 있는지를 확인하는 것이다. 그러나 문맥이 끝나게 되면 문맥의 state (상태 값) 와 그 문맥 자체가 사라지게 된다. 어떤 경우에는 inner function (내부함수) 가 부모 함수의 return 값이 될수도 있다. 이렇게 리턴된 내부 함수가 free variable (자신의 활성 객체에서 찾을 수 없는 identifier)를 가지고 있을 때 활성화(호출)된다면 어떻게 될 것인가? 이와 같은 문제를 해결하기 위한 개념을 closure라 부르며 이것은 scope chain 개념과 연관되어 있다.
Closures
ECMAScript에서 함수는 first-class 객체이다. 이 말은 함수는 다른 함수의 인자로 전달되어 질 수 있다는 것이다(이런 경우 인자로 전달되어 지는 함수를 "functional arguments", 줄여서 "funargs"라고 한다). "funargs"를 인자로 받는 함수는 high-order function (고차원 함수) 또는 수학에 가까운 용어로 operator (연산자) 라고 한다. 또한 함수가 first-class 객체이기 때문에 다른 함수의 리턴 값으로 함수가 리턴 되어 질 수 있다. 다른 함수를 리턴하는 함수를 function valued 함수이라 한다.
"funargs" 와 "functional value"에 연관된 두가지 개념적인 문제가 있다. 이 두 문제는 "Funargs problem" 이라고 불리는 문제로 일반화 될 수 있다(A problem of a functional argument). 그리고 이 "Funargs problem"을 해결하기 위해 closure(클로져)라는 개념이 나타나게 된 것이다. 이 두 개념적 문제에 대해 자세하게 알아보자(이 두 문제가 이전 섹션에서 언급한 함수의 [[Scope]] 속성에 의해 해결된다는 것을 알게 될 것이다).
"Funargs problem" 의 첫번째 문제는 "upward funargs problem" 이다. 이것은 한 함수가 다른 함수에 의해 "up"(caller의 밖으로 리턴)되어 지고 이 리턴된 함수가 free variable 사용하고 있을 경우이다. 부모의 문맥이 종료되어 사라진 이후 리턴되어진 inner function (내부함수)에서 부모 함수의 변수에 접근하기 위해서 inner function이 만들어 질 때 inner function의 [[Scope]] 속성에 부모의 scope chain을 저장해 놓는다. 그리고 이후에 리턴된 inner function이 활성화(호출)되면 inner function의 문맥의 scope chain에는 활성 객체와 [[Scope]] 속성에 저장된 부모의 scope chain이 담기게 되는 것이다.
Scope chain = Activation object + [[Scope]]
다시 한번 강조해야 할 부분은 리턴되는 inner function이 만들어 지는 순간 부모의 scope chain이 저장되어 지는 것이다. 그래야만 나중에 부모 함수(caller)가 사라지더라도 free varible를 찾을수 (look up 혹은 resolve) 있기 때문이다.
function foo() {
var x = 10;
return function bar() {
console.log(x);
};}
// "foo" returns also a function// and this returned function uses// free variable "x"
var returnedFunction = foo();
// global variable "x"var x = 20;
// execution of the returned function
returnedFunction(); // 10, but not 20
var x = 10;
return function bar() {
console.log(x);
};}
// "foo" returns also a function// and this returned function uses// free variable "x"
var returnedFunction = foo();
// global variable "x"var x = 20;
// execution of the returned function
returnedFunction(); // 10, but not 20
이같은 스타일의 scope(스코프)를 static scope 혹은 lexical scope 라고 한다. 위 코드에서 리턴된 bar 함수의 [[Scope]] 속성을 통해서 x 변수가 찾아지는 것을 확인할 수 있다. lexical scope 외에도 dynamic scope 라는 것이 있는데 이 경우 리턴된 bar 함수에서 찾아진 x 변수의 값은 10이 아니라 20이 된다. 그러나 ECMAScript에서는 이 dynamic scope는 사용되지 않는다.
"funargs problem" 중 두번째 문제는 "downward funarg problem" 이다. 부모 문맥은 존재하지만 identifier(식별자)를 찾는데 애매한 경우를 이야기 한다. 다시 말해 문제는 어느 scope(스코프)에 있는 identifier를 사용해야하는 것인가 라는 것이다. 정적으로 함수가 생성될때 저장된 scope(정의 되었을 때의 부모의 scope chain)를 사용해야 하는 건지 실행될때 자신을 호출한 함수의 문맥에 있는 scope(caller 의 scope chain)를 사용해야 하는 건지의 문제인 것이다. 이같은 모호성을 해결하고 closure를 구성하기 위해 static scope(함수가 정의될때의 부모 scope를 사용하는 것)가 사용되는 것으로 정해졌다.
// global "x"var x = 10;
// global functionfunction foo() {
console.log(x);}
(function (funArg) {
// local "x"
var x = 20;
// there is no ambiguity,
// because we use global "x",
// which was statically saved in
// [[Scope]] of the "foo" function,
// but not the "x" of the caller's scope,
// which activates the "funArg"
funArg(); // 10, but not 20
})(foo); // pass "down" foo as a "funarg"
// global functionfunction foo() {
console.log(x);}
(function (funArg) {
// local "x"
var x = 20;
// there is no ambiguity,
// because we use global "x",
// which was statically saved in
// [[Scope]] of the "foo" function,
// but not the "x" of the caller's scope,
// which activates the "funArg"
funArg(); // 10, but not 20
})(foo); // pass "down" foo as a "funarg"
closure를 가지기 위한 필수 조건은 static scope 이다. 그러나 어떤 언어들은 dynamic 과 static scope를 섞어서 사용하기도 한다. ECMAScript는 static scope를 사용하기 때문에 closure가 있고 이는 함수의 [[Scope]] 속성을 이용해서 구현되었다. 아래는 closure의 정확한 정의이다.
closure 는 code block(ECMAScript 에서는 함수를 의미)과 statically(= lexically 정적으로, 그러니까 함수가 생성될 때) 저장된 부모의 scope의 조합이다. 이 조합에서 함수는 free variable을 탐색하게 된다.
모든 함수가 생성 당시 [[Scope]] 속성을 갖게 되므로 이론적으로 모든 함수는 closure이다.
주의해야 할 것은 많은 함수가 똑같은 부모 scope를 공유할 수 있다는 것이다. global 문맥에 정의되어 있는 수많은 함수가 있는 경우가 이러한 예이다. 이 경우 한 함수의 [[Scope]] 속성에 있는 변수를 변경하면 똑같은 부모 scope를 공유하고 있는 다른 함수에도 이것이 반영된다. 곧 부모 scope가 공유되어 있는 closure 사이에는 부모 scope에 있는 변수의 변경이 전체 closure에 반영된다.
function baz() {
var x = 1;
return {
foo: function foo() { return ++x; },
bar: function bar() { return --x; }
};}
var closures = baz();
console.log(
closures.foo(), // 2
closures.bar() // 1);
var x = 1;
return {
foo: function foo() { return ++x; },
bar: function bar() { return --x; }
};}
var closures = baz();
console.log(
closures.foo(), // 2
closures.bar() // 1);
위 코드를 구조화 하면 아래 그림과 같다.
이같은 특징때문에 loop 안에서 여러 함수를 만들 경우 혼란이 생길수도 있다. loop 안에서 counter를 이용하여 함수를 생성할 경우 모든 함수의 counter가 같은 값을 가지게 되된다. 이는 loop 안에 생성된 함수들이 부모 scope의 공유 하는 closure 이기 때문이다.
var data = [];
for (var k = 0; k < 3; k++) {
data[k] = function () {
console.log(k);
};}
data[0](); // 3, but not 0
data[1](); // 3, but not 1
data[2](); // 3, but not 2
for (var k = 0; k < 3; k++) {
data[k] = function () {
console.log(k);
};}
data[0](); // 3, but not 0
data[1](); // 3, but not 1
data[2](); // 3, but not 2
이 같은 문제를 해결하기 위한 여러 가지 방법이 있다. 아래 코드와 같이 생성하고자 하는 함수 위에 부모 함수를 만들어서 새로운 scope를 scope chain에 추가하는 것이다.
var data = [];
for (var k = 0; k < 3; k++) {
data[k] = (function (x) {
return function () {
console.log(x);
};
})(k); // pass "k" value}
// now it is correct
data[0](); // 0
data[1](); // 1
data[2](); // 2
for (var k = 0; k < 3; k++) {
data[k] = (function (x) {
return function () {
console.log(x);
};
})(k); // pass "k" value}
// now it is correct
data[0](); // 0
data[1](); // 1
data[2](); // 2
주의해야 할 점은 ES2015 (=ES6) 에서는 block-scope 가 추가되었다는 것이다. 이전의 ECMAScript에서는 함수 기반의 scope 였는데 ES2015에서 let 과 const 라는 키워드를 추가하여 block 기반의 scope를 도입하였다. 아래 코드가 그 예이다.
let data = [];
for (let k = 0; k < 3; k++) {
data[k] = function () {
console.log(k);
};}
data[0](); // 0
data[1](); // 1
data[2](); // 2
for (let k = 0; k < 3; k++) {
data[k] = function () {
console.log(k);
};}
data[0](); // 0
data[1](); // 1
data[2](); // 2
이제 실행 문맥의 마지막 속성인 "this" 에 대해 알아보자.
This value
this 값은 실행문맥과 연관이 있는 객체이다. 그러므로 this 값은 context object라고 명한다.
그 어떤 객체도 문맥의 this 값에 의해 사용되어 질수 있다. 중요한 점은 this 는 실행 문맥의 속성이지 변수 객체의 속성이 아니라는 것이다.
이는 매우 중요한데 변수와 달리 this 값은 절대 identifier resolution process(식별자를 찾기 위한 sope chain을 뒤지는 과정) 를 거치지 않기 때문이다. 즉 this 값에 접근할때 scope chain 검색 없이 바로 실행 문맥에서 찾는다는 것이다. 이 this 값은 오직 문맥에 들어갈때 한번만 생성된다.
주의: ES2015 에서는 this가 lexical environment의 속성이 되었다. 다시 말해 변수객체의 속성이 되었다는 것이다. 이는 arrow function을 지원하기 위한 것으로 arrow function은 부모 문맥으로 부터 상속받은 lexical this를 가지게 된다(함수가 정의 될 당시의 부모 문맥의 this를 자신의 this로 갖게 된다).
global 문맥에서는 this 값은 global object 자신을 가리키고 위 변수 객체 섹션에서 언급했드시 global object는 곧 global variable object (변수 객체) 이기 때문에 this 값은 global 변수 객체가 된다.
var x = 10;
console.log(
x, // 10
this.x, // 10
console.log(
x, // 10
this.x, // 10
window.x // 10
);
함수 문맥에서는 this 값은 호출될 때마다 달라질수 있다.
// the code of the "foo" function// never changes, but the "this" value// differs in every activation
function foo() {
alert(this);}
// caller activates "foo" (callee) and// provides "this" for the callee
foo(); // global object
foo.prototype.constructor(); // foo.prototype
var bar = {
baz: foo
};
bar.baz(); // bar
(bar.baz)(); // also bar(bar.baz = bar.baz)(); // but here is global object(bar.baz, bar.baz)(); // also global object(false || bar.baz)(); // also global object
var otherFoo = bar.baz;
otherFoo(); // again global object
function foo() {
alert(this);}
// caller activates "foo" (callee) and// provides "this" for the callee
foo(); // global object
foo.prototype.constructor(); // foo.prototype
var bar = {
baz: foo
};
bar.baz(); // bar
(bar.baz)(); // also bar(bar.baz = bar.baz)(); // but here is global object(bar.baz, bar.baz)(); // also global object(false || bar.baz)(); // also global object
var otherFoo = bar.baz;
otherFoo(); // again global object
이유를 좀더 자세하고 알고 싶으면 http://dmitrysoshnikov.com/ecmascript/chapter-3-this/ 를 읽어보길 권하다(번역: http://huns.me/development/258).
이 글에서는 this의 설명이 자세하지 않으니 위 링크를 꼭 읽어보길 바란다. 사실 hoisting 만큼 쉽게 이해가 되지 않는 부분이 this 이기 때문에 위 링크의 글을 읽어보면 조금 더 명확해 질 것이다 (쉽게 기억하는 방법은 this가 포함된 함수를 호출하는 대상 곧 "." 앞의 객체 혹은 함수(예를 들어 someVarible.callFunction() 에서 callFunction 메소드에 this가 들어 있다면 someVariable이 caller, 곧 호출하는 대상)가 있다면 this는 그 대상을 가리키게 되는 것이고 그게 없다면 그냥 global을 의미).
이 글은 Evernote에서 작성되었습니다. Evernote는 하나의 업무 공간입니다. Evernote를 다운로드하세요. |