자바스크립트에서 함수 정의, 호출, 생성자, 클로저에 대해

2012/06/29 23:42




자바스크립트에서 함수를 정의할 때 어떻게 해석되고, 또한 호출할 때는 어떤 과정을 거치는지 이해하는 것을 목표로 합니다. 더불어 이런 이해를 바탕으로 클로저가 성립하는 구체적인 이유를 알 수 있습니다.


자바스크립트 함수 생성


자바스크립트의 함수는 크게 함수 선언문, 함수 표현식이 있는데 형태, 해석시점, 저장되는 메모리 공간이 아래와 같이 다릅니다. 


함수 선언문(Function Declaration: FD) 

- 형태 : function foo() {}

- 해석시점 : 자바스크립트 코드가 로드되고 처음으로 해석되는 시점

- 메모리 공간 : 부모 함수의 스코프 내에 vo(value object)의 key-value 영역


함수 표현식(Function Expression: FE) 

- 형태 : 함수 선언문이 아닌 모든 형태. 

  a = function foo() {};//기명 함수 표현식 

  a = function() {};//무명 함수 표현식

  (function foo(){})();//기명 즉시 실행 함수 표현식

  (function (){})(); //무명 즉시 실행 함수 표현식  

- 해석시점 : 자바스크립트 코드가 실행중일 때 

- 메모리 공간 : 컨텍스트 영역의 해쉬맵의 key-value 영역 



이처럼 선언문과 표현식은 차이가 있지만 자바스크립트 해석기가 해석할 때는 아래처럼 해석합니다.


function foo(a, b) {

     var c, d;

     c = 10;

     this.k = 10;

     function bar() {

     }

     d = function () {

     };

}


위 함수는 대략 아래와 같이 해석됩니다. 


foo = {};

foo.prototype = {constructor:foo};

foo.[[scope]] = new [[Scope]]; //스코프 생성. 결국 해쉬맵이고 key-value공간이다. 

foo.[[scope]].__parent__ = [EC(실행 컨텍스트, execution context) 참조. 여기선 window 함수의 EC] 

foo.[[scope]].a = 0; //arg 첫번째 인자

foo.[[scope]].b = 1; //arg 두번째 인자 

foo.[[scope]].c = null; //var c

foo.[[scope]].d = null; //var d

foo.[[scope]].bar = null; //함수 선언문(function declaration). 꼭 var bar; 가 선언된 것과 비슷한 처리를 한다.

foo.[[scope]].__parent__.foo = <foo>function;  //위 함수가 함수 선언문으로 정의되어 window의 EC에 key-value로 등록됨. 만약 함수 표현식인 foo = function( a, b ) {}; 였다면 window['foo'] = foo; 처럼 해석될 것임 


함수가 해석될때 위처럼 함수에 대한 메모리 공간을 확보하고 스코프 영역을 만듭니다. [[scope]]에 등록된 것은 함수의 인자(arg 부분)와 내부변수 선언(var 부분), 그리고 함수 선언문(bar 함수)입니다. 호이스팅(hoisting) 비밀은 결국 자바스크립트 인터프리터가 함수를 해석할 때 이렇게 생성된 스코프 영역에 변수를 key-value로 잡는 것일 뿐입니다. 함수의 정의가 해석될 때는 호이스팅 되는 대상에 value가 지정되지 않습니다. 이것은 foo 함수가 실행되면서 생성되는 foo.[[scope]]의 clone인 foo함수의 EC가 생성된 다음에 함수 내부가 실행되면서 할당됩니다. 


c = 10과 d = function() {}; 부분이 foo.[[scope]]에 따로 등록되지 않음을 확인하세요. 즉, 이것들은 호이스팅 대상이 아닙니다. 


this.k = 10도 함수가 호출되지 않으면 아무일도 일어나지 않습니다. this라는 것 자체가 실행 컨텍스트인 인스턴스를 가리키는 것이기에 함수가 실행중이 아니라면 이 부분은 실행되지도 않겠지요.  


함수가 객체임을 여기서도 알 수 있습니다. 처음 foo = {}는 그것은 말하고 있는 것입니다. 결국 전체적으로 볼 때, 함수가 해석되면 Object의 인스턴스를 하나 만드는 이상의 비용이 듭니다.  


함수는 일급객체(first-class)로 동적으로 생성되고 어떤 변수에 할당되거나 확장하거나 복사될 수 있으며 다른 함수의 인자로 전달할 수 있고 반환될 수 있습니다. 또한 자신의 프로퍼티와 메서드도 가질 수 있습니다. 


foo.[[scope]].__parent__부분은 정의된 함수의 바깥에 정의된 함수의 EC를 참고합니다. EC의 정체는 함수가 호출될 때 만들어지며 [[scope]]의 clone입니다. 위 예제의 경우 window 함수내에서 foo 함수의 정의부가 해석되는 것이므로 foo.[[scope]].__parent__가 window.[[scope]].clone()인 EC가 됩니다. 


자바스크립트의 경우 특별히 window는 객체이자 함수입니다. window는 글로벌 객체인 동시에 글로벌 함수인 셈입니다. 이 window 함수에도 [[scope]]가 있습니다. 여기에 정의된 var, 함수 선언문은 전부 window 함수의 [[scope]]에 자리잡게 됩니다. 자바스크립트 인터프리터는 window.[[scope]].__parent__ = null 로 강제로 지정하여 스코프 체인 검색의 고리를 끊는 역할을 합니다.  


함수의 정의에 key-value 등록


함수도 결국 Object 객체의 key-value 공간입니다. 


function foo() {

}


이 있다면 자바스크립트 인터프리터는는 다음처럼 해석합니다.


foo = {};

foo.prototype = {constructor:foo};

foo.[[scope]] = new [[Scope]]; //스코프 생성. 결국 해쉬맵이고 key-value공간이다. 

foo.[[scope]].__parent__ = [EC. 여기선 window 함수의 EC ] 

foo.[[scope]].__parent__.foo = foo;  //함수 선언문으로 정의되어 window의 EC에 key-value로 등록됨. 만약 함수 표현식인 foo = function( a, b ) {}; 였다면 window['foo'] = foo; 처럼 해석될 것임. 


주목할 것은 함수의 정의가 인터프리터에 의해 해석될 때 Object객체의 key-value 공간과 Function 객체의 스코프 key-value 공간을 만든다는 사실입니다. 스코프 공간인 [[scope]]는 외부접근이 불가능하다. 하지만 foo = {} 처럼 함수는 그 자체가 Object 객체의 key-value 해쉬맵 형태이므로 우리는 다음과 같은 일도 할 수 있습니다.


foo.test = 10; 


이것만은 반드시 기억하세요. 자바스크립트의 메모리 공간 동적으로 생성되며 Object객체의 key-value 공간과 Function 객체의 스코프 key-value 공간이 있으며 각종 인자, 값, 함수, 로직등이 모두 이 공간내에서 처리된다는 사실을 말입니다.


자바스크립트 함수 호출 


함수가 실제로 적용(호출)된다면 자바스크립트 인터프리터는 어떻게 동작할까요? 이것도 간단한 예를 들어보지요. 아래 함수 선언문은 위에서 예로 들었던 것과 같습니다.  


function foo(a, b) {

     var c, d; //호이스팅 대상 

     c = 10;

     this.k = 10;

     bar(); 

     //함수 선언문, 호이스팅 대상 

     function bar() {

     }

     //함수 표현식, 호이스팅 대상 아님 

     d = function () {

     };

     d();

}



위 함수는 아래 처럼 해석되어 처리됩니다.


foo = {};

foo.prototype = {constructor:foo};

foo.[[scope]] = new [[Scope]]; //스코프 생성. 결국 해쉬맵이고 key-value공간이다. 

foo.[[scope]].__parent__ = [EC. 여기선 window 함수의 EC ] 

foo.[[scope]].a = 0;

foo.[[scope]].b = 1;

foo.[[scope]].c = null;

foo.[[scope]].d = null;

foo.[[scope]].bar = null; //함수 선언문은 함수의 이름을 기반으로 var bar;이 선언된 것과 같은 동작을 한다. 

foo.[[scope]].__parent__.foo = foo; 



이 함수를 호출해보죠.


foo( 1, 3 );


위 함수는 자바스크립트 인터프리터는 아래처럼 인식합니다. 


window.foo.apply( null, [1, 3] );


이 때 하는 일은 다음과 같습니다. 


EC<foo> = foo.[[scope]].clone(); //실행할 함수의 스코프 영역을 복사 

EC<foo>.this = context || window; //context는 apply의 첫번째 인자값을 가리킨다. 이 값이 null인 경우에는 기본적으로 window가 된다. 

EC<foo>.arguments = {'0': 1, '1': 3, 'length': 2}; //넘겨준 인자 설정 

EC<foo>.a = EC.arguments[0]; //첫번째 인자를 vo의 a에 할당

EC<foo>.b = EC.arguments[1]; //두번째 인자를 vo의 b에 할당 

//여기부터 실제 함수 실행부분이다. 

EC<foo>.bar = <bar> function; //함수의 선언문이 해석되어 EC<foo>.bar에 key-value로 등록. 

EC<foo>.c = 10;  //함수내  c에 10을 할당 

EC<foo>.this.k = 10; //실행 컨텍스트인 window에 k = 10을 key-value로 잡음 

EC<foo>.bar.apply( null, [] ); //bar 함수 선언문 호출 

EC<foo>.d = <무명>function; //함수 표현식으로 정의된 d = function() {}; 부분 해석 

EC<foo>.d.apply( null, [] ); //d 함수 표현식 호출


자바스크립트 함수가 호출되는 것이 아니라 적용되는 것임을 여기서 더 분명해졌습니다. apply()가 실행되는 순간 foo 함수가 해석될 때 생성된 [[scope]]가 전부 복사(clone)되며 인자값을 EC<foo>의 key-value로 등록됩니다. 


foo 함수에 정의된 함수의 선언문인 bar()는 foo 함수 내 구문이 실행되기 전에 최초로 EC<foo>에 key로 미리 잡힌 EC.<foo>.bar 에 bar 함수의 정의를 등록합니다. 결국 bar 함수 선언문이 어디에 있던지 foo 함수 내에서는 호출이 가능합니다. 함수의 선언문의 정의 부분까지 호이스팅 됨을 보여주고 있습니다.  


c = 10의 경우 foo.[[scope]]에 key-value로 c가 등록되어 있으므로 그의 복사본인 EC<foo>.c에 10을 할당하도록 해석합니다. 


this.k = 10에서 this는 window와 바인딩 되었으므로 window.k = 10처럼 동작한다. 


bar();의 경우 자신의 스코프 영역에 있는 bar를 검색해서 실행합니다. 결국 EC<foo>.bar()가 호출되는 것과 동일합니다. 이것은 결국 EC.bar.apply( null, [] );이 됩니다. 


d는 함수 표현식입니다. var d는 이미 foo.[[scope]].d = null이지만 이 함수 정의는 foo 함수의 내부가 실행될 때 할당됩니다. 즉 EC<foo>.d = <무명>function 처리 됩니다. 


d();는 자신의 스코프 영역에 있는 d를 검색해서 실행합니다. 결국 EC<foo>.d()가 실행되는 것과 같으며 이는 EC<foo>.d.apply( null, [] ); 과 동일합니다. 


스코프 체인에 대한 다른 설명은 다음 링크를 참고하세요.

http://dmitrysoshnikov.com/ecmascript/javascript-the-core/#scope-chain 


자바스크립트의 key-value 탐색 : 스코프 체인 검색과 프로토타입 검색


자바스크립트의 탐색 우선순위는 아래와 같습니다.


  1. EC에 지정된 key-value 
  2. EC.__parent__ 에 지정된 key-value 
  3. EC.this에 지정된 key-value
  4. EC.this.prototype에 지정된 key-value 


1, 2는 스코프 체인 검색이라고 하고 3, 4는 프로토타입 체인 검색이라고 합니다. 둘다 체인 검색이다라는 점에서 속도면에서는 이득이 없지만 탐색 우선순위가 스코프 영역에 더 있으므로 여기에 key-value를 지정하는 것이 프로토타입 영역보다 탐색 속도상에서 이득을 볼 수 있습니다. 즉 var로 선언된 변수, 함수 선언문, 인자들로 구성된 스코프 영역이 key-value 탐색에 이득이 있습니다. 


함수내에서 this를 사용하는 것과 사용하지 않은 것은 극명하게 차이가 있습니다. 'this.속성'은 현재 실행 컨텍스트인 인스턴스로부터 key-value 탐색을 한다는 것을 지칭합니다. 이것을 붙이지 않으면 함수의 스코프 탐색이 되고, 스코프 체인 검색에서 해당 속성이 없으면 바로 글로벌 객체인 window의 key-value 탐색이 됩니다. 반대로 this를 사용하면 현재 실행 컨텍스트로부터 key-value 검색으로 하고 없으면 프로토타입 체인 검색을 실시합니다. 이 점을 이해하기 위해 몇 개의 예제를 보겠습니다. 


var k = 4;

var a = {

     foo: function() {

          var k = 1;

          alert(k); //1

     }

};

a.foo();


위 경우는 결과가 1입니다. this없이 k를 탐색하는데 있어서 현재 함수의 현재 스코프에 정의되어 있는 k를 바로 찾을 수 있기 때문입니다.


var k = 4;

var a = {

     k: 3,

     foo: function() {

          alert(k); //4

     }

};

a.foo();


위 경우는 결과가 4입니다. this없이 k를 탐색은 현재 스코프가 아닌 부모 함수의 스코프 체인을 타고 올라가기 때문에 결국 k=4를 찾게 됩니다.


k = 4;

var a = {

     k: 3,

     foo: function() {

          alert(k); //4

     }

};

a.foo();


k가 window 함수의 스코프 영역이 아닌 window객체의 key-value로 선언되었습니다. 함수 내부에서 this없이 k 탐색은 일단 스코프 검색을 통해 찾지만 못찾습니다. 이때 this.k를 명시하지 않았기 때문에 k는 곧바로 window 객체부터 k를 찾습니다. 이렇게 함수내에서 this없이 속성을 탐색했을 때 스코프 체인 검색후 바로 window의 key-value 탐색이 이뤄지는 것은 자바스크립트가 예외로 허용합니다. 다른 말로 window만 스코프 체인 검색과 프로토타입 체인 검색을 실행합니다. window는 전역이자 특별한 객체이자 함수인 셈입니다.


k = 4;

var a = {

     k: 3,

     foo: function() {

          var k = 9;

          alert(this.k); //3

     }

};

a.foo();


위 예제처럼 함수 내에 k가 함수의 스코프 영역에 정의됨에도 불구하고 this.k 처럼 this를 사용하면 스코프 체인 검색은 하지 않습니다. 바로 this인 a의 key-value검색에 들어갑니다. 결국 a.k가 정의되어 있으므로 결과는 3이 됩니다.


k = 4;

var a = {

     foo: function() {

          var k = 9;

          alert(this.k); // undefined

     }

};

a.foo();


위 예제는 this.k시 this인 a의 key-value 검색을 통해 k를 찾지 못하는 경우입니다. 실제로 a에 key-value로 k가 정의되어 있지 않습니다. 이런 경우 프로토타입 체인 검색에 들어갑니다. 이 경우 a의 프로토타입은 Object.prototype입니다. Object의 프로토타입에도 k가 없으므로 결과는 undefined입니다.


k = 4;

var Func = function() {

     this.b = 3;

     this.foo = function() {

          var k = 9;

          alert(this.k); // 10

          alert(this.b); // 3

     }

};

Func.prototype.k = 10;

a = new Func();

a.foo();


위 예제에서 this.k는 a.k를 탐색하는 것과 같지만 실제로 a의 key-value에 k가 등록되어 있지 않습니다. 이 경우에 a의 프로토타입(a.__proto__ === Func.prototype)인 Func으로부터 k를 탐색합니다. Func.prototype.k = 10을 지정했으므로 결국 찾게 되어 this.k는 10이 됩니다. 만약 Func.prototype.k가 지정되어 있지 않는다면 a.__proto__.__proto__는 Object.prototype 이기 때문에 k를 찾지 못합니다. 결국 undefined가 됩니다. 


스코프 체인 검색이 프로토타입 체인 검색보다  우선순위가 높다고 해서 무조건 스코프 영역에 key-value를 잡는 것이 좋은 것은 아닙니다. 이것도 결국 체인 검색이기 때문에 없으면 [[scope]].__parent__를 타고가 결국 window.[[scope]] 까지 거슬러 탐색합니다. 또한 함수가 호출될 때마다 EC = foo.[[scope]].clone() 처리가 된다는 점을 상기해본다면 여기에서 EC.arguments 를 생성하고 인자변수인 EC.a, EC.b 에 인자값을 할당하는 일을 합니다. 이러한 과정을 함수가 호출될 때마다 하는 것이기 때문에 함수 호출자체가 좋지 못하죠. 하지만 함수를 쓰지 않는 것은 말도 안되는 일이므로 함수 호출자체는 어쩔 수 없더라도 function foo(a, b) 처럼 만들어서 EC.a, EC.b에 매번 할당하는 과정을 거치지 않도록 하는게 좋습니다. 어짜피 자바스크립트의 함수는 동적 인자를 받아들이므로 다음처럼 써도 문제가 없습니다.


function foo( /*a, b*/) {

     return arguments[0] + arguments[1];

}

alert( foo( 3, 4 ) ); //7


foo() 함수가 매우 자주 호출된다면 전체적인 속도 개선을 위해서 고려해 봄직한 방법입니다. 



자바스크립트 함수를 통한 객체 생성. new 연산자 사용 


자바스크립트로 인스턴스를 생성한다는 차원은 다른 언어에서 클래스를 기반으로하는 객체 생성과는 근본적으로 다릅니다. 일단 자바스크립트는 클래스가 존재하지 않습니다. 전부 함수일 뿐이며 또 함수를 생성자 삼아서 new 키워드를 통해 인스턴스를 생성합니다. 


자바스크립트에서 new 키워드를 통해 인스턴스를 생성한다는 의미는 function을 object로 만들겠다는 의미입니다. 실제로 typeof Object나 typeof Array의 결과는 function이고 여기에 new 연산자 만든 인스턴스에 typeof obj를 하면 object가 나옵니다. 


위에서 정의한 함수를 다시 한번 보지요. 


function foo(a, b) {

     var c, d;

     c = 10;

     this.k = 10;

     bar();

     function bar() {

     }

     d = function () {

     };

     d();

}



자 위 함수를 이제 new를 통해 인스턴스를 만들어 보겠습니다. 이때 함수 foo는 생성자 또는 생성자 함수라고 지칭합니다. 


f = new foo(1, 3);


참고로 생성자에 대해서 다음 링크를 참고하세요.

http://dmitrysoshnikov.com/ecmascript/javascript-the-core/#constructor


위 코드는 다음과 같이 해석되고 구동됩니다.


f = {}; //해쉬맵 생성. 여기서는 window['f'] = {} 와 같다. 

f.constructor = foo; //constructor키에 함수 객체를 참조. f생성을 어떤 것으로 했는가 척도가 됨 

f.__proto__ = f.constructor.prototype; //foo 객체에 만들어진 프로토타입을 __proto__키의 값으로 참조 

f.constructor.apply( f, [1, 3] ); //함수 실행. 실행 컨텍스트로 삼을 f를 첫번째 인자로 넘김 


와! 해쉬맵을 생성한 다음 f에 할당합니다. 이 해쉬맵에 construtor와 __proto__를 key-value로 등록하고 f.constructor인 foo 함수를 호출합니다. 이 때 실행 컨텍스트로 함수 foo의 인스턴스인 f로 지정합니다. 


f.constructor.apply( f, [1, 3] ); 부분만 따로 해석해 보겠습니다.


EC = f.constructor.[[scope]].clone(); //실행할 함수의 스코프 영역을 복사 

EC.this = context || window; //context는 apply의 첫번째 인자값으로 실행 컨텍스트를 가리킨다. 이 값이 null인 경우에는 기본적으로 window가 된다. 여기선 f가 된다. 

EC.arguments = {'0': 1, '1': 3, 'length': 2}; //넘겨준 인자 설정 

EC.a = EC.arguments[0]; //첫번째 인자를 vo의 a에 할당

EC.b = EC.arguments[1]; //두번째 인자를 vo의 b에 할당 

//함수 내부 실행 

EC.c = 10;  //함수 vo의 c에 10을 할당 

EC.this.k = 10; //실행 컨텍스트인 f에 k = 10을 key-value로 잡음 

EC.bar.apply( null, [] ); //bar 함수 선언문 호출 

EC.d = <무명>function; //함수 표현식으로 정의된 d = function() {}; 부분 해석 

EC.d.apply( null, [] ); //d 함수 표현식 호출


와… 이전 함수를 호출할 때와 거의 흡사합니다. 다른 점은 실행 컨텍스트가 함수 foo의 인스턴스인 f라는 점입니다. 그래서 EC.this = f가 되고 EC.this.k = 10은 f.k = 10 처럼 됩니다. 


이 쯤되면 컨텍스트라는 개념이 이해갈지 모르겠습니다. 컨텍스트는 new 연산자를 통해 만들어진 해쉬맵 메모리 공간을 의미하며 함수 실행시 this라는 키워드와 바인딩되는 것입니다. 함수가 생성에 관여하든 호출되던지 결국 apply 함수가 호출되는 것은 동일합니다. 이 때 컨텍스트는 함수 실행 때 다른 컨텍스트로 바꿀 수 있습니다. 인스턴스라고 잡은 넘이 바로 컨텍스트가 되는 겁니다. 


이 말을 확실히 집어보면 결국 new 연산자라는 것은 this를 바인딩 시킬수 있는 컨텍스트(context)를 heap 메모리에 생성시킨다는 의미입니다. 더욱 더 풀어본다면, new 연산자는 heap 메모리에 빈 hash 맵( a = {} )을 생성하고 그 heap을 컨텍스트에 실어서 생성자 함수를 호출(apply, call)하서 함수 내부 로직을 실행해 필요하다면(this.k = 3 처럼) hash맵에 key-value를 할당하는 역할을 합니다.  


여러가지로 볼 때 OOP 기반의 언어의 new와 자바스크립트의 new가 얼마나 다른지 확인할 수 있습니다. 그래서 OOP기반 언어의 개념으로 설명하는 자바스크립트 책을 보면 이해가 안되는 부분이 많은가 봅니다. 


아래글을 읽으면 더 이해가 쉬울겁니다.

클래스 기반 언어 vs 자바스크립트 1/3 : http://zero.diebuster.com/zero/?p=36 


클로저(Closure)


자바스크립트의 유효영역는 함수 레벨 영역(function level scope) 입니다. 그래서 var로 선언된 지역변수(variable object)와 함수 선언문(function declaration)은 함수 레벨 영역으로 호이스팅 됩니다. for나 if의 블록 레벨의 지역변수 도 결국 함수 레벨로 호이스팅 됩니다. 호이스팅은 끌어올리기라는 의미보다 실제로는 결국 함수에 [[scope]]라는 직접접근이 불가능한 key-value 해쉬맵을 만들어 var, 함수 선언문을 이 공간에 잡는다는 의미가 있다고 이미 언급했습니다. 


이 함수 레벨 영역의 [[scope]]에는 __parent__라는 키값이 잡히고 EC를 참조합니다. 이 말을 이해하는게 중요한데요. 다음 예제들 보겠습니다.  


var a = 7;

function f1() {

     var a = 10;

     function f0() {

          console.log(a); // 10

          console.log( 'f0() this=' +this); // window

     }

     console.log( 'f1() this=' + this); // window

     f0(); //10 

}

f1();


위 예제는 다중으로 함수가 중첩되어 있습니다. 이쯤 되면 이 함수가 어떻게 해석될지 스스로 써볼 수 있어야 겠지요? 


//window 함수 해석. window는 자바스크립트에서 유일하게 객체이자 함수이다.  

window = {};

window.prototype = {constructor:window};

window.[[scope]] = new [[Scope]];

window.[[scope]].__parent__ = null; //window 함수가 글로벌 함수로 더이상의 __parent__에 해당하는 EC는 존재하지 않는다. 그러므로 null로 지정. 

window.[[scope]].a = null; //전역영역(window)에 선언된 var a 

window.[[scope]].f1 = null; //전역영역(window)에 선언된 f1 함수 선언문이 참조될 곳


//window 함수 실행 

window.apply( null, [] );


window함수가 드디어 실행됩니다. 이때 기억을 더듬어 본다면 EC는 window.[[scope]].clone() 입니다. window.apply()를 수행하면 다음과 같은 일을 합니다. 


//window 함수 실행과 종료 

EC = window.[[scope]].clone(); //실행할 함수의 스코프 영역을 복사 

EC.this = context || window; //여기선 window다. 

EC.arguments = {'length': 0};  

EC.f1 = <f1>function; //f1 함수 선언문이 해석됩니다.

EC.a = 7; //var a에 7을 할당  

EC.f1.apply( null, [] );  //f1 함수 실행 


EC에 전역영역에 선언된 var a = 7과 f1 함수가 해석되어 key-value로 잡힙니다. 이 EC는 window.[[scope]].clone() 이라는 것을 항상 기억하세요.


중간에 EC.f1 = <f1>function 부분은 아래처럼 해석됩니다. 


//f1 함수 선언문 해석 

EC.f1 = {};

f1 = EC.f1;

f1.prototype = {constructor: f1};

f1.[[scope]] = new [[Scope]];

f1.[[scope]].__parent__ = EC; 

f1.[[scope]].a = null; //f1 함수영역에서 선언된 var a 

f1.[[scope]].f0 = null; //f1 함수영역에 선언된 f0 함수 선언문이 참조될 곳


EC.f1.apply( null, [] ); 부분에 의해 f1 함수가 실행됩니다.

결국 다음과 같은 일을 하게 됩니다.


//f1 함수 실행과 종료 

EC<f1> = f1.[[scope]].clone();

EC<f1>.this = context || window; //여기선 window

EC<f1>.arguments = {'length': 0}

EC<f1>.a =  10; //f1함수의 var a에 10을 할당 

EC<f1>.f0 = <f0>function; //f1 함수 선언문 해석해 f0 이름으로 f1의 EC에 할당 

console.log( 'f1() this=' +this); // window

EC<f1>.f0.apply( null, [] ); //f0 함수 실행. 


EC<f1>은 f1 함수의 EC입니다. EC.f1.apply( null, [] )로 함수를 실행했으므로 EC<f1>은 f1.[[scope]].clone()이 됩니다. 하지만 첫번째 인자로 null을 인자로 넘기므로 this는 window가 됩니다. 여기서도 f1함수에 선언한 var a에 의해 EC<f1>.a에 10을 할당하고 f0 함수 선언문을 해석해 EC<f1>의 key-value로 등록합니다. 


이 또한 ECf1.f0 = <f0>function은 다음과 같이 해석됩니다. 


//f0 함수 선언문 해석 

EC<f1>.f0 = {};

f0 = EC<f1>.f0;

f0.prototype = {constructor: f0};

f0.[[scope]] = new [[Scope]];

f0.[[scope]].__parent__ = EC<f1>;


EC<f1>.f0.apply( null, [] ) 부분에 의해 f0가 다음처럼 실행됩니다.


//f0 함수 실행과 종료 

EC<f0> = f0.[[scope]].clone();

EC<f0>.this = context || window; //여기선 window 

EC<f0>.arguments = {'length': 0}

console.log(a); // 10

console.log( 'f0() this=' +this); // window


console.log(a)에서 a는 this를 붙이지 않았으므로 스코프 체인 탐색을 실시합니다. 


//console.log(a) 실행

1단계 : EC<f0>에 에 a가 있는가 찾음. 결과 없음

2단계 : EC<f0>.__parent__인 EC<f1>에 a가 있는가 찾음. 있음!

결국 console.log(a)는 10이 됨. 


자식함수의 스코프에 해당하는 EC<f0>의 key-value에는 a가 없지만 부모 함수의 스코프 영역인 EC<f0>.__parent__에 참조된 EC<f1>로부터 a를 찾아냅니다. 만약 EC<f1>에 a가 없다면 EC<f1>.__parent__로부터 window의 EC에서 또 검색하고 여기에도 없으면 EC.__parent__를 참조하려고 하지만 window의 EC.__parent__는 강제로 null이 할당되어 있으므로 찾을 수 없습니다. 여기가 a 검색의 끝이 되지요. 이러한 체인 검색을 바로 스코프 체인 검색이라고 합니다. 


클로저를 알아보겠습니다. 클로저는 방금 언급한 결국 부모함수(f1)와 자식함수(f0) 간에 스코프 체인에서 부터 비롯됩니다. 자식 함수인 f0는 부모 함수 f1의 EC<f1>을 EC<f0>.__parent__를 통해 그 존재를 알고 있습니다. 즉 클로저라는 말은 자신의 스코프(scope, 유효영역) 밖에 있는 EC에 접근할 수 있는 f0와 같은 함수를 지칭합니다. f0함수는 자신의 스코프에 a가 없더라도 그 부모 함수인 f1의 스코프를 알고 있기 때문에 f0는 클로저가 됩니다. 


특별히 window 함수의 스코프는 전역영역으로 더 이상 부모함수가 있지 않습니다. EC.__parent__ = null 인 것이 이것을 의미합니다. 그래서 window 함수는 클로저라고 말할 수 없지만 구조적으로는 클로저라고 할 수 있습니다. 


결국 자바스크립트의 함수는 전부 클로저인 셈입니다. 


지금까지 예를 조금 다르게 예제를 들어보겠습니다.


var a = 7;

function f0() {

     console.log(a);

}

function f1(f) {

     var a = 10;

     f(); //7

}

f1(f0);


이것은 왜 결과가 7일까요? f0는 f1이 아닌 window가 부모함수이므로 EC<f0>.__parent__는 EC입니다. f1실행시 f0를 인자로 넘기지만 이 사실은 변함이 없으므로 f0의 window 함수의 스코프를 참조하여 클로저를 형성합니다. 


클로저에 대해서는 다음 글도 참고하세요.

http://dmitrysoshnikov.com/ecmascript/javascript-the-core/#closures



클로저를 왜 쓸까요? 

자바스크립트의 특징 때문에 클로저라는 개념도 생기게 됨을 알았습니다. 그럼 클로저를 쓰는 이유는 무엇일까요? 많은 책에서 private 변수를 생성할 수 있다등의 여러 말들이 있지만 정작 진짜 필요한 이유는 다른데 있지 않은가 생각됩니다.


클로저는 함수를 특정인자와 함께 호출하기 위해 만들어 사용합니다.


예를 들어 alert('test')를 인자없이 test()만 호출해도 같은 동작을 하도록 하려면 어떻게 할까요?


makeA= ( function( a ) {

     return function() {

          alert(a);

     };   

})();


test = makeA('test');

test();


이렇게 하면 됩니다. 여기서 a가 클로저의 영역이 됨을 알 수 있습니다. makeA() 함수는 호출할 때마다 자식함수를 생성해서 반환해줍니다. 이 모든게 가능한 것은 함수가 클로저인 동시에 자바스크립트의 함수가 일급객체라는 점 때문에 동적으로 생성하고 반환할 수 있는 것이겠지요. 


이렇게 만들어진 test는 a라는 인자를 EC<test>.__parent__에 품고 있는 클로저 함수가 됩니다. 중요한 것은 처음 alert()함수는 인자를 넘긴 반면 test 함수는 인자를 넘기지 않아도 언제든지 a = 'test'가 되어 있어서 alert(a)를 호출하는 효과를 가지게 된다는 점입니다.  


이게 왜 유용할까요?


div.addEventListener( 'click', test );


이런게 된다는 점입니다. '인자가 있는 함수'를 '인자를 기억하도록 바꿀 수 있는 함수'로 바꾸는데 클로저의 역할은 매우 큽니다. 클로저는 커링(Curring)을 할 수 있는 이유이기도 합니다. 


글쓴이 : 지돌스타(http://blog.jidolstar.com/819)

저작자 표시 비영리 동일 조건 변경 허락

JavaScript , , , , , , , , , , , ,

  1. 그렇다면 foo() 내부에 정의된 bar() 의 해석 시점은 foo()가 실행될 때가 되는거군요?

    모든 FD 가 코드 로딩 시 해석 시점에 해석되는 건 아니라고 생각하면 되겠죠?