Scope chains 과 scope 프로퍼티 제대로 이해하기!!

아래 포스팅과 이어서 이번에는 Scope chains 과 [[scope]] 프로퍼티에 대해서 제대로 한번 알아보자.

원문 출처: http://www.jibbering.com/faq/faq_notes/closures.html

함수 호출을 위한 실행 문맥과 스코프 체인은 함수 객체의 [[scope]] 프로퍼티에 정의된 실행 문맥의 Activation/Variable 객체를 스코프의 맨 앞쪽에 더함으로써 생성되어진다. 그래서, 내부적으로 [[scope]] 프로퍼티가 어떻게 정의되는지 이해하는 것은 매우 중요하다.

ECMAScrpt 에서 함수(function)은 객체다. 그리고 이런 객체들은 변수 인스턴스화 과정을 거치면서 함수 선언에 의해 만들어지거나, 함수표현식을 평가할때, 혹은 함수 생성자에 의해서 invoking 을 수행할때 만들어진다.

Function 생성자와 함께 생성되는 함수(Function) 객체는 항상 전역객체를 포함하고 있는 스코프 체인(scope chain)을 가르키는 [[scope]] 프로퍼티를 가진다.

함수 선언식이나 표현식과 함께 생성되는 함수 객체는 그 실행 문맥안에서 스코프 체인을 가진다. 그리고 이런 함수 객체들은 그 실행문맥 안에서 내부적인 [[scope]] 프로퍼티를 생성하고 할당한다.

쉬운 예로 아래와 같은 전역 함수 선언식을 보자( 표현식 아님!!  표현식은 그 다음 예제..)

function exampleFunction(formalParameter) {
    ….    // function body code
}

위 함수는 전역 실행 문맥안에서 변수 인스턴스화 과정을 통해 함수 객체로 생성된다.
전역 실행 문맥은 오직 전역 객체만을 가지는 스코프 체인을 가진다. 그래서 이 함수는 “exampleFunction” 이라는 이름을 가지는 전역 객체의 프로퍼티로 생성되어 지고, 내부적으로  [[scope]] 프로퍼티가 할당되어 진다. 이 [[scope]]프로퍼티는 오직 전역 객체만을 포함하고 있는 스코프 체인을 가르키게 된다.

함수 표현식이 전역 실행 문맥 안에서 수행될때, 비슷한 스코프 체인이 할당되어진다.
다음 예제를 보자.

var exampleFuncRef = function() {
   … // function body code
}

위와 같은 경우를 제외하고 전역 객체의 이름있는 프로퍼티들은 전역 실행 문맥에서 변수 인스턴스화 과정을 거치면서 생성되어 진다. 하지만 위 예제의 함수 객체는 인스턴스화 과정에서 생성되는것이 아니라 변수에 할당된 함수 표현식이 평가 될때 생성된다. 그러므로 인스턴스화 과정을 거칠때 위 예제의 함수는 전역 객체에 의해 참조 되지 않는다.

하지만 함수 객체의 생성은 여전히 전역 실행 문맥안에서 수행 되기때문에 이 생성된 함수 객체의 [[scope]] 프로퍼티는 할당된 스코프 체인 안에서 여전히 전역 객체만을 포함하게 된다.
 
내부 함수(중첩된 함수) 선언식이나 표현식은 한 함수의 실행문맥 안에서 함수 객체가 생성되어 지는 결과를 가져온다. 그래서 보다 복잡한 스코프 체인이 되어버린다.
아래 코드를 보자.

function exampleOuterFunction (formalParameter) {
   function exampleInnerFunctionDec() {
      … // inner function body
   }
     …. // the rest of the outer function body.
}
exampleOuterFunction(5);

밖에 선언된 함수의 객체는 전역 실행 문맥의 변수 인스턴스화 과정에서 만들어지기 때문에 그 함수(exampleOuterFunction) 객체의[[scope]] 프로퍼티는 전역객체만을 가지는 스코프체인을 가르키게 된다.

전역코드가 exampleOuterFunction(5); 이 구문을 수행할때, exampleOuterFunction 함수가 호출되고 이것은 이 함수를 위한 새로운 함수 실행 문맥을 만들어주게 된다. 그리고 앞서서 설명했던것처럼, Activation 과 Variable 객체가 그 실행 문맥 안에 만들어지게 된다.

이때 새로운 실행문맥의 스코프는 새로운 Activation 객체를 포함하는 체인을 구성하게 된다. 그리고 이 체인은 Activation 객체에 이어서 호출된 본래 함수 즉, exampleOuterFunction 객체의 [[scope]] 프로퍼티를  참조하게 되기때문에, Activation객체에서 exampleOuterFunction 객체 [[scope]] 프로퍼티가 가르키는 전역 객체 까지 체인을 형성하게 되는 것이다.

새로운 실행 문맥의 변수 인스턴스화 과정을 거치게 되면서 exampleInnerFunctionDec 함수 객체가 생성이 되고, exampleInnerFunctionDec 함수 객체의 [[scope]]프로퍼티는 현재의 실행 문맥 안에서 생성된 scope를 할당받는다.
즉, exampleInnerFunctionDec 함수 객체의 [[scope]]는 현재 실행 문맥의 Activation 객체와 전역객체인 window 객체까지 체인을 형성할수 있게 된다.

지금까지 소스코드와 구조에 따라서 자동적으로 컨트롤 되는 모습을 보았다. 실행 문맥의 스코프 체인은 새로운 실행문맥에서 함수 객체의 [[scope]] 프로퍼티가 생성되고, 본래 함수 객체의  [[scope]] 프로퍼티들이 가르키는 실행 문맥의 스코프안의 객체까지 연결될수 있음을 나타낸다. 하지만 ECMAScipt 는 with문을 통해서 스코프 체인을 수정할수 있다.

with 문이 표현식을 평가했을때, 만약에 그 표현식이 객체라면, 그 객체는 현재 실행문맥의 스코프 체인안에 추가된다.(추가되는 객체는 현재 스코프 체인의 맨 앞에 있는 Activation 객체 앞에 추가가 된다.) 그리고 with 문 다음의 문장들을 수행하고, 완료한뒤에 본래의 스코프 체인을 복구한다. (아~ — 스코프 체인안에 객체에 추가하고 복구하는 과정때문에 속도가 느려지는군…–_)

with문은 변수 인스턴스화 과정중에 함수의 객체가 생성되기 때문에 함수 선언식에는 영향을 주지 않는다. 하지만 함수 표현식은 with 문에 의해서 평가 되어 질수 있다.

/* 전역 변수 y를 만들고 그것이 하나의 객체를 참조한다. */
var y = {x:5}; // x 프로퍼티를 가지는 객체 리터럴
function exampleFuncWith(){
    var z;
    /* 전역 변수에 의해 참조 되는 객체를 y에 추가하고 그것을 스코프 체인 앞에 둔다.    */
    with(y){
        /* 함수 객체를 생성하기 위해 함수식을 평가하고, 지역 변수 z를 함수 객체의 참조로 할당한다.
        */

        z = function(){
            … // inner function expression body;
        }
    }
    …
}
/* execute the – exampleFuncWith – function:- */
exampleFuncWith();

exampleFuncWith 함수가 호출되어 질때, 그 결과의 실행 문맥은 그 실행문맥 안의 Activation 객체에서  전역 변수까지 참조하는 스코프 체인을 가진다. 그리고 with문을 수행할때 전역 변수 y가 참조하는 객체를 이 스코프 체인 맨 앞에 추가한다.
즉, y객체 –> Activation 객체 –> window 객체 순으로 체인이 생긴다.

함수 표현식(z=function(){})의 의해 평가되서 생성된 함수 객체(z)의 [[scope]] 프로퍼티는 현재의 스코프를 가르키게 된다. 즉, y객체를 포함하고, 뒤이어 Activation 객체 그리고 외부함수 실행문맥이 가지는 스코프 영역의 전역객체 즉, window 객체까지 체인을 이루게 된다.

with 문 블락이 종료되면, 현재의 실행 문맥은 복구되지만,( 스코프 맨 앞에 있던 y 객체가 지워진다.) 이미 생성된 함수 객체(z)의 [[scope]] 프로퍼티는 여전히 y 객체를 가지는 스코프 체인을 가지고 있는다.