[번역] ECMA-262-3 in detail. Chapter 4. Scope chain.

Posted by in Research

원문 출처 : ECMA-262-3 in detail. Chapter 4. Scope chain.. by Dmitry Soshnikov

 

 

 

소개(Introduction)


변수 객체를 살펴보았던 두 번째 챕터에서 보았듯이, 실행 콘텍스트의 데이터(변수, 함수 선언 그리고 함수의 형식 매개변수)는 변수 객체의 프로퍼티로 저장된다.

그리고 콘텍스트로 진입할 때 매번 초기값을 갖는 변수 객체를 생성하며, 코드 실행 단계에서 값을 갱신한다는 사실도 배웠다.

이 챕터에서 실행 콘텍스트와 직접적으로 관련있는 내용 한 가지를 더 자세히 알아볼텐데, 이번 주제는 바로 스코프 체인(scope chain)이다.

 

 

 

정의(Definitions)


간략하게 요점만 살펴보면, 스코프 체인은 대게 중첩 함수와 관련이 있다.

다들 알고 있듯이, ECMAScript는 중첩 함수를 허용하며 심지어 부모 함수가 이러한 중첩 함수를 결과 값으로 반환할 수도 있다.

 

그 결과, 모든 콘텍스트는 자신의 고유 변수 객체를 가질 수 있다. 전역 콘텍스트는 자기 자신을 변수 객체로 가지며, 함수 콘텍스트는 활성화 객체를 갖는다.

그리고 스코프 체인은 내부 콘텍스트가 이용하는 모든(부모) 변수 객체의 리스트다. 변수를 검색할 때 이 체인을 이용한다. 즉, 위의 예제에서 “bar” 콘텍스트의 스코프 체인은  AO(bar), AO(foo), VO(global)를 갖는다.

이 내용을 자세하게 확인해보자.

우선 스코프 체인을 정의한 다음에 예제를 갖고서 더 깊게 알아보겠다.

 

스코프 체인은 실행 콘텍스트와 관련있으며, 식별자 해석 시 변수 검색에 이용하는 변수 객체의 체인이다.

 

함수 콘텍스트의 스코프 체인은 함수를 호출할 때 생성되며, 활성화 객체와 함수의 내부 [[Scope]] 프로퍼티를 가진다. 밑에서 함수의 [[Scope]] 프로퍼티를 자세하게 설명하겠다.

 

개략적인 콘텍스트의 내부는 이러하다.

 

이에 따른 스코프의 정의는 다음과 같다.

 

예를 들기 위해서 스코프와 [[Scope]]를 ECMAScript의 일반 배열로 나타낼 수 있다.

 

다른 구조적인 관점에서 볼 때, 체인이 모두 연결되어 있는 상태에서 부모 스코프(부모의 변수 객체)를 참조하는 계층적인 객체 체인으로 표현할 수 있다. 이 관점은 변수 객체를 다룬 두번째 챕터에서 설명했던 __parent__ 개념과 부합한다.

 

하지만 배열을 이용하여 스코프 체인을 표현하는 것이 훨씬 쉽기 때문에 이 방식으로 접근할 생각이다. 게다가, 이에 대한 스펙(10.1.4)은 구현 수준에서 __parent__ 기능을 포함하는 계층구조 체인으로 접근하는 방식을 이용할 수 있는 것과는 상관없이 “스코프 체인은 객체의 리스트”라고 추상적으로 서술하고 있다.

아래에서 설명할 AO + [[Scope]]의 결합 그리고 식별자 해석 프로세스는 함수의 라이프 사이클과 관련이 있다.

 

 

 

함수 라이프 사이클(Function life cycle)


함수의 라이프 사이클은 생성 단계, 활성화 단계(call)의 2가지로 나뉜다. 이를 자세하게 살펴보자.

 

 

함수 생성

 

알려진 것처럼, 콘텍스트 단계로 들어갈 때 변수/활성화 객체(VO/AO)가 함수 선언으로 들어간다. 예제에 나와있는 전역 콘텍스트의 변수와 함수 선언을 보자.(전역 콘텍스트에서 변수 객체는 전역 객체 자신이라는 사실 기억하는가?)

 

함수가 활성화되면, 정확하게(기대한 대로) 30이 출력되는 것을 볼 수 있다. 여기에 아주 중요한 특징 하나가 있다.

이전까지는 현재 콘텍스트의 변수 객체에 대해서만 이야기 했다. 여기에서 변수 “y”는 함수 “foo”(“foo” 콘텍스트의 AO에 있다는 의미)에 정의되어 있지만, 변수 “x”는 “foo”의 콘텍스트에 정의되어 있지 않으므로 “foo”의 AO에 추가되지 않는다는 것을 알 수 있다. 언뜻 봤을 때 변수 “x”는  “foo” 함수에 존재하지 않는다. 이 다음에 설명하겠지만 이것은 오직 언뜻 봤을 때의 이야기다.  “foo” 콘텍스트의 활성화 객체는 오직 하나의 프로퍼티 “y”만을 갖는다.

 

어떻게 함수 “foo”가 변수 “x”에 접근할 수 있을까? 함수가 자신보다 더 상위에 있는 콘텍스트의 변수 객체에 접근할 수 있다고 가정하는 것이 논리적이다. 이 가정은 정확하게 들어맞는다. 실제 물리적으로 이 메커니즘은 함수 내부의 [[Scope]] 프로퍼티를 이용해서 구현하고 있다.

 

[[Scope]] 는 현재 함수 콘텍스트의 상위에 있는 모든 부모 변수 객체의 계층 체인이다. 이 체인은 함수가 생성될 때 함수에 저장된다.

 

함수를 생성할 때 [[Scope]] 프로퍼티가 함수에 저장되는데, 일단 한 번 저장되고 나면 함수가 사라질 때까지 정적으로 변하지 않는다는 사실을 주목하자. 함수를 결코 호출할 수 없어도, 함수 객체는 이미 [[Scope]] 프로퍼티를 가지고 있다.

생각해야 할 또 한 가지는 스코프(스코프 체인)와는 다르게 [[Scope]]는 콘텍스트가 아닌 함수의 프로퍼티라는 점이다. 위 예제에서, “foo” 함수의 [[Scope]]를 아래와 같이 나타낼 수 있다.

 

다음으로 함수를 호출하면 활성화 객체가 만들어지고, this 값과 스코프(스코프 체인)를 결정하는 함수 콘텍스트 진입 단계로 들어간다. 이 단계에 대해서 이야기해보자.

 

 

함수 활성화

 

스코프를 정의(Definitions)할 때 이야기 했듯이, 콘텍스트로 진입하고 AO/VO가 만들어진 후에, 콘텍스트의 Scope 프로퍼티(변수 검색를 위한 스코프 체인)는 다음과 같이 정의된다.

 

여기에서 강조할 부분은 활성화 객체가 Scope 배열의 첫 번째 원소로 스코프 체인의 가장 앞에 온다는 점이다.

 

이는 식별자 해석 과정에 있어 아주 중요한 특징이다.

 

식별자 해석은 변수(또는 함수 선언)가 스코프 체인의 어떤 변수 객체에 속하는지를 결정하는 과정이다.

 

이 알고리즘의 결과로 레퍼런스 타입 값을 얻을 수 있는데, 이 레퍼런스 타입의 구성요소 중 base는 변수 객체(또는 변수를 찾지 못 했다면 null)에 해당하며, property name은 검색(해석)한 식별자의 이름이다. 레퍼런스 타입은 Chapter 3. This.에서 자세하게 설명했다.

식별자 해석 과정은 변수의 이름에 해당하는 프로퍼티를 검색 하는 과정을 포함하며, 스코프 체인 가장 깊은 곳에 있는 콘텍스트의 변수 객체부터 시작해서 가장 위에 있는 변수 객체까지 연속적으로 검사하는 과정이다.

그 결과 현재 콘텍스트의 지역 변수는 부모 콘텍스트에 있는 변수보다 검색 우선 순위를 가지며, 이름이 같지만 서로 다른 콘텍스트에 존재하는 두 변수의 경우, 더 깊은 콘텍스트에 있는 변수가 우선한다.

 

위에서 설명한 예제에 중첩 수준을 더 추가해서 약간 복잡하게 만들어보자.

 

이 예제 코드는 다음의 변수/활성화 객체, 함수의 [[Scope]] 프로퍼티 그리고 콘텍스트의 스코프 체인을 갖는다.

 

전역 콘텍스트의 변수 객체 :

 

“foo” 생성 시점에 “foo”의 [[Scope]] 프로퍼티 :

 

“foo” 함수의 활성화 시점(콘텍스트로 진입하는 단계)에 “foo” 콘텍스트의 활성화 객체 :

 

“foo” 콘텍스트의 스코프 체인 :

 

중첩된 “bar” 함수가 생성되는 시점에 “bar”함수의 [[Scope]] :

 

“bar” 활성화 시점에 “bar” 콘텍스트의 활성화 객체 :

 

“bar” 콘텍스트의 스코프 체인 :

 

“x”, “y”, “z”의 식별자 해석 :

 

 

 

스코프의 특징(Scope features)


스코프 체인과 함수의 [[Scope]] 프로퍼티에 대한 몇가지 중요한 특징을 알아보자.

 

 

클로저(Closures)

 

ECMAScript의 클로저는 [[Scope]] 프로퍼티와 직접적으로 관련이 있다. [[Scope]]는 함수를 생성할 때 함수에 저장되어서, 함수 객체가 사라질 때까지 존재한다. 실제로, 클로저는 정확하게 함수 코드와 [[Scope]] 프로퍼티의 조합이다. 따라서, [[Scope]]는 함수가 만들어진 곳의 어휘적 환경(부모 변수 객체)을 갖는다. 함수가 활성화되고 난 후에 상위의 콘텍스트에 있는 변수를 이 변수 객체의 어휘적 체인(생성 단계에서 정적으로 만들어진) 안에서 찾을 수 있다.

예제를 보자.

 

변수 “x”는 foo 함수의 [[Scope]]에 있는 것을 알 수 있다. 변수를 검색할 때, 함수 호출 시점의 동적인 체인(이 경우 변수 “x”의 값은 20이 될 것이다)이 아닌, 함수 생성 순간에 정의된 어휘적인 체인을 이용하였다.

클로저에 대한 또 다른 예제를 보자.

 

위이 예제에서도 역시 식별자 해석에 함수 생성 시점에 정의된 어휘적 스코프 체인을 이용하였다. 변수 “x”를 30이 아닌 10으로 해석했다. 게다가, 이 예제는 함수의 [[Scope]](함수 “foo”가 반환한 익명 함수의 경우에)가 심지어 생성된 함수의 콘텍스트가 이미 종료되고 난 이후에도 존재하고 있음을 명확하게 보여준다.

클로저와 ECMAScript의 클로저 구현 대한 이론은 Chapter 6. Closures에 더 자세한 내용이 나와있다.

 

 

Function 생성자로 생성한 함수의 [[Scope]]

 

위의 예제에서 함수 생성시에 [[Scope]] 프로퍼티를 가져오고 이 프로퍼티를 통해서 모든 부모 콘텍스트의 변수에 접근한다는 것을 보았다. 그러나, 이 규칙에는 한가지 중요한 예외가 있는데, Function 생성자를 이용해서 함수를 생성하는 경우다.

 

Function 생성자를 이용해서 생성한 “barFn” 함수는 변수 “y”에 접근할 수 없다. 그러나 이것이 함수 “barFn”이 내부에 [[Scope]] 프로퍼티를 가지고 있지 않다는 것을 의미하는 것은 아니다(아니라면 변수 “x”에 접근할 수 없어야 한다). Function 생성자를 이용해서 생성한 함수의 [[Scope]] 프로퍼티는 항상 전역 객체를 포함한다는 것이 중요하다. 이러한 특징을 통해서 보면, 상위 문맥의 클로저를 만들고자 할 때 전역 객체를 제외할 수 없다.

 

 

2차원 스코프 체인 검색

 

또한, 스코프 체인 검색의 중요한 포인트는 ECMAScript의 프로토타입적인 성격 때문에 변수 객체의 프토토타입 또한 고려해야 한다는 점이다. 객체 내에서 직접적으로 프로퍼티를 찾지 못한다면, 프로토타입 체인까지 검색을 수행한다. 즉, 일종의 2차원 체인 검색인 셈이다. (1) 스코프 체인 연결, (2) 그리고 깊은 프로토타입 체인 연결에 있는 모든 스코프 체인 연결을 검색한다. Object.prototype에 프로퍼티를 정의해서 이 내용을 확인할 수 있다.

 

활성화 객체는 프로토타입을 가지고 있지 않다는 것을 아래의 예제에서 알 수 있다.

 

“bar” 함수 콘텍스트의 활성화 객체가 프로토타입을 가지고 있다면, 프로퍼티 “x”는 AO 안에서 직접 해석될 수 없으므로  Object.prototype 안에서 해석되어야 한다. 그러나 위에 있는 첫번째 예제에서, 식별자 해석을 위해 스코프 체인을 탐색하는 과정에서 Object.prototype를 상속한 전역 객체(모든 구현이 다 그러한 것은 아님)에 도달했고, 따라서 “x”는 10으로 해석 되었다.

SpiderMoneky 일부 버전의 기명 함수 표현식(named function expressions, 줄여서 NFE )에서 유사한 상황을 찾을 수 있다. 이 경우 함수 표현식에 선택적으로 부여한 이름을 저장하는 특별한 객체가 Object.prototype을 상속한다. 또한 Blackberry의 일부 버전에서는 활성화 객체가 Object.prototype을 상속한다. 이 특징에 대한 더 세부적인 내용은 Chapter 5. Function에서 다루겠다.

 

 

전역 콘텍스트와 eval 콘텍스트의 스코프 체인

 

이 내용은 아주 흥미로운 이야기는 아니지만, 알아둘 필요는 있다. 전역 콘텍스트의 스코프 체인은 오직 전역 객체만을 갖는다. 그리고 “eval” 코드의 콘텍스트는 호출 콘텍스트와 같은 스코프 체인을 갖는다.

 

 

 

코드 실행 중 스코프 체인에 영향을 미치기

 

ECMAScript에는 코드 실행 런타임에 스코프 체인을 변경할 수 있는 두 가지 구문이 있다.   with문과 catch절이다. 둘 다 이들 구문 내에 나타나는 식별자를 찾기 위한 객체를 스코프 체인의 가장 앞에 추가한다. 이 중에 하나를 코드에 적용하면, 스코프 체인은 개략적으로 다음과 같이 변경된다.

 

with문의 경우에는 파라미터로 넘겨 받은 객체를 추가한다(그 결과, 이 객체의 프로퍼티에 접두사를 붙이지 않고 접근할 수 있다)

 

스코프 체인이 아래와 같이 변경된다.

 

with문이 스코프 체인 가장 앞에 추가한 객체에서 식별자가 처리되는 것을 볼 수 있다.

 

무슨 일이 일어난 걸까? 콘텍스트 진입 단계에서, “x”와 “y” 식별자를 변수 객체에 추가한다. 다음으로, 런타임에 코드 실행 단계에서 다음의 변경이 일어난다.

 

  1. x = 10, y = 10
  2. 객체 { x : 20 }을 스코프 체인의 앞에 추가한다.
  3. 콘텍스트 진입 단계에서 모든 변수를 해석하고 추가했기 때문에 with 내에서 var 구문을 만났을 때 아무 것도 만들지 않는다.
  4. 오직 “x”의 값을 수정하는데, 정확하게는 두번째 단계에서 스코프 체인의 앞에 추가된 객체 내에서 해석되는 “x”를 말한다. 20이었던 x의 값이 10이 된다.
  5. 또한 위의 변수 객체 내에서 해석되는 “y”도 변경한다. 결과적으로 10이었던 “y”의 값이 30이 된다.
  6. 다음으로 with문이 종료된 후에, 스페셜 객체는 스코프 체인에서 제거된다(“x”의 값이 변경되고, 30 또한 객체에서 제거된다). 즉, 스코프 체인 구조가 with문에 의해서 확장되기 이전 상태로 돌아온다.
  7. 마지막에 있는 두 번의 alert 호출을 통해서 알 수 있듯이, 현재 변수 객체 내에 있는 “x”의 값은 같은 상태로 남아있고, “y”의 값은 with문 내에서 변경한 상태 그대로 30이다.

 

catch절 또한 exception 파라미터에 접근하기 위해서 exception 파라미터의 이름을 유일한 프로퍼티로 갖는 중간 스코프 객체를 만들며, 이 객체를 스코프 체인의 앞에 추가한다. 개략적으로 아래와 같이 나타낼 수 있다.

 

스코프 체인이 변경된 상태를 다음과 같이 나타낼 수 있다.

 

catch절 내의 작업이 종료된 후에, 스코프 체인은 이전 상태로 돌아온다.

 

 

 

결론(Conclusion)


이번 단계에서, 실행 콘텍스트에 관계된 거의 모든 일반적인 개념과 세부내용에 대해서 알아보았다. 다음에는 계획한 대로, 함수 객체를 자세하게 분석해서 함수의 종류(FunctionDeclaration, FunctionExpression)와 클로저에 대해서 알아보겠다. 클로저는 이번 글에서 다룬 [[Scope]] 프로퍼티와 직접적으로 관련이 있긴 하지만 적당한 챕터에서 다룰 생각이다. 궁금한 점은 댓글로 남겨주면 감사하겠다.

 

 

 

추가 문헌(Additional literature)


8.6.2 – [[Scope]]

10.1.4 – Scope Chain and Identifier Resolution