원문 출처 : ECMA-262-3 in detail. Chapter 2. Variable object by Dmitry Soshnikov
 
 
 
소개(Introduction)


 
프로그램을 개발할 때 우리는 항상 함수와 변수를 선언하고 이것을 이용해서 성공적으로 시스템을 구축한다. 그런데 인터프리터는 어떻게, 어디에서 데이터(함수, 변수)를 찾을까? 우리가 필요한 객체를 참조할 때 무슨 일이 일어나는 걸까?
많은 ECMAScript 프로그래머들이 변수와 실행 콘텍스트가 서로 밀접하게 연관되어 있다는 사실을 알고 있다.
 

var a = 10; // 전역 객체의 변수
(function () {
  var b = 20; // 함수 콘텍스트의 지역 변수
})();
alert(a); // 10
alert(b); // "b" is not defined

 
또한 현재 버전의 ECMA 스펙에서는 오직 “함수” 코드의 실행 콘텍스트만이 격리된 유효범위(scope)를 만들 수 있다는 것도 알고 있다.
이것은 C/C++의 경우와는 반대이다. 예를 들어 보면, ECMAScript의 for 루프 블럭은 지역 콘텍스트를 만들지 않는다.
 

for (var k in {a: 1, b: 2}) {
  alert(k);
}
alert(k); // 루프가 종료된 후에도 변수 k는 여전히 유효범위 안에 들어 있다.

 
우리가 데이터를 선언할 때 무슨 일이 일어나는지 좀 더 자세한 내용을 알아보자.
 
 
데이터 선언(Data declaration)


 
만약 변수가 실행 콘텍스트와 관련이 있다면, 어디에 데이터가 저장되고 어떻게 그것들을 가져올 수 있는지 알아야 할 필요가 있다. 이런 메카니즘을 변수 객체(Variable object)라고 부른다.
 

변수 객체(이하 VO)는 실행 콘텍스트와 관련 있는 특별한 객체이며 아래의 것들을 저장한다.
 
– 변수(var, VariableDEclaration)
– 함수 선언(FunctionDeclaration, 이하 FD)
– 그리고 함수 형식 매개변수들(function formal parameters) 

 

 ES5에서는 변수 객체의 개념이 어휘적 환경(lexical environments) 모델로 대체 되었다. 세부적인 설명은 적당한 챕터에서 하겠다.

 
개략적으로 예를 들어보면, 변수 객체를 보통의 ECMAScript 객체로 표현할 수 있다.
 

VO = {};

 
그리고 우리가 이야기 했듯이, VO는 실행 콘텍스트의 프로퍼티다.
 

activeExecutionContext = {
  VO: {
    // 콘텍스트 데이터 (var, FD, function arguments)
  }
};

 
오직 전역 콘텍스트(전역 객체 자신이 바로 변수 객체이다.)의 변수 객체에 있는 변수만 간접적으로 참조(VO의 프로퍼티명을 통해서) 할 수 있다. 다른 콘텍스트에서는 직접 VO 객체를 참조할 수 없으며, 이것은 순수하게 구현 메커니즘에 대한 이야기다.
우리가 변수나 함수를 선언하는 것은 VO에 변수의 이름과 이름에 할당된 값을 갖는 새로운 프로퍼티를 만드는 것과 같다.
 
예제 :

var a = 10;
function test(x) {
  var b = 20;
};
test(30);

 
이에 대한 변수 객체:

// 전역 콘텍스트의 변수 객쳋
VO(globalContext) = {
  a: 10,
  test: <reference to function>
};
// test 함수 콘텍스트의 변수 객체
VO(test functionContext) = {
  x: 30,
  b: 20
};

 
그러나 구현 수준(그리고 스펙)에서 변수 객체는 추상적으로 본질적인 존재이다.  물리적으로, 실제 구현된 실행 콘텍스트에서 VO는 다른 이름과 다른 초기 구조를 가진다.
 
 
다른 실행 콘텍스트의 변수 객체(Variable Object in different execution contexts)


 
일부 작업(예를 들어, 변수 초기화)과 변수 객체의 동작은 모든 종류의 실행 콘텍스트에서 공통적이다. 이러한 관점에서, 변수 객체를 추상화된 기본형으로 보는 것이 편리하다. 함수 콘텍스트는 또한 변수 객체와 관련있는 추가적인 세부 내용을 정의할 수 있다.
 

AbstractVO (generic behavior of the variable instantiation process)


╠══> GlobalContextVO
║ (VO === this === global)

╚══> FunctionContextVO
(VO === AO, <arguments> object and <formal parameters> are added

 
자세하게 알아보도록 하자.
 
 
전역 콘텍스트의 변수 객체(Variable object in global context)


 
여기에서, 우선 전역 객체(Global object)를 정의하자.
 

전역 객체란 어떠한 실행 콘텍스트로 들어가기 전에 생성되는 객체이다. 전역 객체는 단일 사본으로 존재하며, 이 객체의 프로퍼티는 프로그램의 어떠한 곳에서도 접근할 수 있다. 프로그램이 종료되면 전역 객체의 라이프 사이클은 끝이 난다.

 
생성 단계에서 전역 객체는 초기화되면서 Math, String, Date, parseInt 등의 프로퍼티를 갖게 되고, 또한 자신을 참조할 수 있는 추가적인 객체를 갖는다. 예를 들어, BOM(Browser Object Model)에서 전역 객체의 window 프로퍼티는 전역 객체를 참조한다.(모든 구현이 이러한 것은 아니다.)

global = {
  Math: <...>,
  String: <...>
  ...
  ...
  window: global
};

 
전역 객체의 프로퍼티를 참조할 때 전역 객체는 직접 전역 객체의 이름으로 접근할 수 없기 때문에 흔히 접두사는 생략된다. 그러나 전역 콘텍스트의 this 값으로 전역 객체에 접근할 수 있고, 또한 BOM에서 window 프로퍼티를 이용하는 것과 같이 재귀적으로 자기 자신을 참조하는 방법을 통해서도 가능하다. 간단하게 아래와 같이 쓴다.
 

String(10); // global.String(10); 을 의미한다.
// 접두사를 이용
window.a = 10; // === global.window.a = 10 === global.a = 10;
this.b = 20; // global.b = 20;

 
그러면, 전역 콘텍스트의 변수 객체로 돌아와서, 여기에서 변수 객체는 전역 객체 자신이다.

VO(globalContext) === global;

 
이러한 이유로 전역 콘텍스트에서 변수를 선언하면, 우리는 변수 객체의 프로퍼티를 통해서 간접적으로 접근할 수 있기 때문에(변수명을 미리 알 수가 없는 경우에)  이 사실을 정확하게 이해할 필요가 있다.

var a = new String('test');
alert(a); // VO(globalContext)에 직접적으로 접근한다. "test" 출력
alert(window['a']); // 전역 === VO(globalContext)인 점을 이용해서 간접적으로 접근한다. "test" 출력
alert(a === this.a); // true
var aKey = 'a';
alert(window[aKey]); // 동적인 프로퍼티 명으로 간접적으로 접근. "test" 출력

 
 
함수 콘텍스트의 변수 객체(Variable object in function context)


 
함수의 실행 콘텍스트에서 VO는 직접적으로 접근이 불가능하고 이 역할은 소위 이야기하는 활성화 객체(Activation object, 이하 AO)가 맡는다.

VO(functionContext) === AO;

 

 활성화 객체는 함수의 콘텍스트로 들어가면서 생성되고 값이 Argumets 객체인 arguments 프로퍼티로 초기화된다.

 

AO = {
  arguments: <ArgO>
};

 
Arguments 객체는 활성화 객체의 프로퍼티다. 활성화 객체는 다음의 프로퍼티들을 갖는다.
 
callee – 현재 함수에 대한 참조
length – 실제로 전달된 인자의 수량
properties-indexes(string으로 변환된 integer) – 이 값은 함수 매개 변수의 값(왼쪽부터 오른쪽까지의 매개변수 리스트)이다. properties-indexes의 개수는 arguments.length 와 같다. arguments 객체의 properties-indexes와 제공된 형식 매개 변수는(실제로 전달된) 값을 공유한다.
 

function foo(x, y, z) {
  // 정의된 함수 매개변수 x,y,z의 수량
  alert(foo.length); // 3
  // 실제로 전달된 매개변수(x, y)의 수량
  alert(arguments.length); // 2
  // 함수 자신에 대한 참조
  alert(arguments.callee === foo); // true
  // 파라미터 공유
  alert(x === arguments[0]); // true
  alert(x); // 10
  arguments[0] = 20;
  alert(x); // 20
  x = 30;
  alert(arguments[0]); // 30
  // 그러나, arguments 객체의 index-property와 관련 있는
  // 전달되지 않은 매개변수 z의 경우는 공유되지 않는다.
  z = 40;
  alert(arguments[2]); // undefined
  arguments[2] = 50;
  alert(z); // 40
}
foo(10, 20);

 
위의 예제에서 가장 마지막 내용은 구글 크롬의 구형 버전에서 파라미터 z와 arguments[2]가 공유되는 버그가 존재한다.
 

 ES5에서 활성화 객체의 개념 역시 어휘적 환경(lexical environments)의 공통 단일 모델로 대체 된다.

 
 
콘텍스트 코드 실행 단계(Phases of processing the context code)


 
이제 우리는 이 글의 중요한 지점에 도착했다. 실행 콘텍스트 코드의 실행은 2가지 기본적인 단계로 나뉜다.

  1. 실행 콘텍스트 진입
  2. 코드 실행

변수 객체의 변경은 이 두 단계와 밀접하게 연관되어 있다.
이 두 단계의 실행은 일반적인 동작이며, 콘텍스트의 종류와는 별개라는 것을 명심하자.(즉, 전역 콘텍스트과 함수 콘텍스트 둘 모두에 적용된다.)
 
 
실행 콘텍스트 진입(Entering the execution context)


 
실행 콘텍스트로 들어가면(하지만 코드가 실행되기 전인…) , VO는 아래의 프로퍼티들로 채워진다. (이미 처음에 설명한 것들이다.)
 

함수의 각 형식 매개 변수를 위한 프로퍼티(함수 콘텍스트의 경우)

– 형식 매개변수의 이름과 값을 갖는 변수 객체의 프로퍼티가 생긴다. 값이 전달되지 않은 경우에는 형식 매개변수의 이름을 갖되, 값은 undefined인 VO의 프로퍼티가 생긴다.

 

각 함수 선언을 위한 프로퍼티(FunctionDeclaration, FD)

– 함수 객체의 이름과 값을 갖는 변수 객체의 프로퍼티가 생긴다. 변수 객체가 이미 같은 이름을 갖고 있다면 새로운 값으로 교체된다.

 

각 변수 선언을 위한 프로퍼티(var, VariableDeclaration)

– 변수의 이름과 undefined 값을 갖는 변수 객체의 프로퍼티가 생긴다. 변수의 이름이 이미 선언된 형식 매개변수나 함수의 이름과 같다면, 변수 선언은 무시된다.

 
예제를 통해서 알아보자.
 

function test(a, b) {
  var c = 10;
  function d() {}
  var e = function _e() {};
  (function x() {});
}
test(10); // 호출

 
전달된 매개변수 10을 갖는 test 함수 콘텍스트로 진입하면, AO는 다음과 같다.
 

AO(test) = {
  a: 10,
  b: undefined,
  c: undefined,
  d: <reference to FunctionDeclaration "d">
  e: undefined
};

 
AO가 함수 x를 갖고 있지 않다는 것을 주목하자. 이것은 함수 x가 함수 선언이 아니라 VO에 영향을 주지 않는 함수 표현(FunctionExpression, 이하 FE)이기 때문이다.
그러나 함수 _e 역시 함수 표현이지만, 우리가 아래에서 보게 될 것처럼, 변수 e에 할당되기 때문에 e라는 이름을 통해서 접근할 수 있다. 함수 선언과 함수 표현의 차이는 Chapter 5. Functions에서 깊게 다룬다.
그리고 이제 콘텍스트 코드 실행의 두 번째 단계인 “코드 실행 단계”로 들어간다.
 
 
코드 실행(Code execution)


 
코드가 실행될 즈음에는 AO/VO는 이미 프로퍼티들로 채워져 있다.(하지만 모든 프로퍼티에 전달한 실제 값이 할당되어 있지는 않으며, 아직 대부분의 값이 undefined로 초기화 되어 있는 상태이다.)
같은 예제를 놓고 볼 때, 코드 해석 동안에 AO/VO는 다음과 같이 변경된다.
 

AO['c'] = 10;
AO['e'] = <reference to FunctionExpression "_e">;

 
다시 한 번 함수 표현식 _e가 선언된 변수 e에 저장되어 있다는 이유 때문에 여전히 메모리에 존재하고 있다는 사실을 주목하자. 그러나 함수 표현식 x는 AO/VO에 존재하지 않는다. x를 정의하기 전이나 정의한 후에도 마찬가지로 우리가 함수 x를 호출하려 하면, “x” is not defined.  라는 에러를 보게 된다. 변수에 저장하지 않은 함수 표현식은 그것이 정의된 곳에서 호출하지 않으면 재귀적인 방법으로만 호출할 수 있다.
 
한 가지 예제(전통적으로 쓰이는)를 더 보자.
 

alert(x); // function
var x = 10;
alert(x); // 10
x = 20;
function x() {}
alert(x); // 20

 
 
 
변수에 대해서(About variables)


 
종종 Javascript에 대한 다양한 글과 심지어  책에서 “전역 콘텍스트에서 var 키워드를 이용하거나, 다른 어떤 곳에서 var 키워드를 이용하지 않음으로써 전역 변수를 선언할 수 있다.” 고 주장한다. 이것은 틀렸다. 다음의 사실을 기억하자.
 

변수는 오직 var 키워드를 이용해서 선언한다.

 
그리고 아래와 같은 할당은 단지 전역 객체에 새로운 프로퍼티(변수가 아니라)를 만들 뿐이다.
 

a = 10;

 
“변수가 아니다” 라는 말은 변할 수 없다는 의미가 아니라 ECMAScript에서 이야기하는 “변수”의 개념이 아니라는 뜻이다.  (VO(globalContext) === global 이기 때문에, 전역에 선언된 변수는 또한 전역 객체의 프로퍼티가 된다. 기억하겠지?)
그리고 다음의 차이가 있다.(예제를 통해서 보자)
 

alert(a); // undefined
alert(b); // "b" is not defined
b = 10;
var a = 20;

 
모든 것은 다시 VO와 그것의 변경 단계에 달려있다.(콘텍스트 진입 단계와 코드 실행 단계)
 
콘텍스트 진입 :

VO = {
  a: undefined
};

 
이 단계에서 b가 변수가 아니므로 VO 안에 없다는 것을 알 수 있다. b는 코드 실행 단계가 되어서야 나타날 것이다.(에러가 날 것이기 때문에, 위의 예제는 사실 진행이 불가능하다.)
코드를 변경해 보자.
 

alert(a); // undefined, 이유는 알고 있다.
b = 10;
alert(b); // 10, 코드 실행 단계에 만들어진 값.
var a = 20;
alert(a); // 20, 코드 실행 단계에 변경된 값.

 
변수와 관련해서 중요한 것이 하나 더 있다. 단순 프로퍼티와 대조되는 개념인 변수는 {DontDelete} 속성을 갖는다. 이것은 delete 연산자를 이용해서 변수를 삭제할 수 없다는 것을 의미한다.
 

a = 10;
alert(window.a); // 10
alert(delete a); // true
alert(window.a); // undefined
var b = 20;
alert(window.b); // 20
alert(delete b); // false
alert(window.b); // 여전히 20

 

메모 : ES5에서 {DontDelete}는 [[Configurable]]로 이름이 변경되었고 Object.defineProperty 메서드를 이용해서 수동으로 조작할 수 있다.

 
그러나 이 규칙에 영향을 받지 않는 실행 콘텍스트가 하나 있다. 바로 eval 콘텍스트이다. 이 콘텍스트에서는 변수에 {DontDelete} 속성이 설정되지 않는다.
 

eval('var a = 10;');
alert(window.a); // 10
alert(delete a); // true
alert(window.a); // undefined

 
일부 디버그 툴의 콘솔(Firebug)에서 이러한 예제를 테스트 하는 사람들을 위해서 덧붙이자면, Firebug 또한 콘솔에서 코드를 실행하기 위해서 eval을 사용한다. 그래서 변수이 {DontDelete} 속성이 설정되지 않아 삭제될 수 있다.
 
 
 
__parent__ 프로퍼티 (Feature of implementations: property __parent__)


 
이미 알아봤듯이, 표준에 따라 직접 활성화 객체에 접근하는 것은 불가능하다. 하지만 일부 구현 즉, SpiderMonkey나 Rhino에서 함수는 __parent__ 라는 특별한 프로퍼티를 갖는데, 이 프로퍼티는 함수가 만들어진 곳의 활성화 객체(또는 전역 변수 객체)를 참조한다.
 
예제(SpiderMonkey, Rhino)
 

var global = this;
var a = 10;
function foo() {}
alert(foo.__parent__); // global
var VO = foo.__parent__;
alert(VO.a); // 10
alert(VO === global); // true

 
위의 예제에서 foo 함수가 전역 콘텍스트에서 만들어지고, 따라서 __parent__ 프로퍼티에 전역 콘텍스트의 변수 객체, 즉 전역 객체가 설정된다는 것을 알 수 있다.
그러나 SpiderMonkey에서 같은 방식으로 활성화 객체를 가져오기는 불가능하다. 버전에 따라서, 내부 함수의 __parent__는 null이나 전역 객체를 돌려준다.
Rhino에서는 같은 방식으로 활성화 객체에 접근할 수 있다.
 
Example(Rhino)

var global = this;
var x = 10;
(function foo() {
  var y = 20;
  // "foo" 콘텍스트의 활성화 객체
  var AO = (function () {}).__parent__;
  print(AO.y); // 20
  // 현재 활성화 객체의 __parent__는 이미 전역 객체,
  // 즉 이른바 유효범위 체인이라는 변수 객체의 특별한 체인이 형성된다.
  print(AO.__parent__ === global); // true
  print(AO.__parent__.x); // 10
})();

 
결론(Conclusion)


 
이번 글에서 우리는 실행 콘텍스트와 관련있는 객체에 대해서 더 많이 배웠다. 이 자료가 유용하고 당신이 이전에 갖고 있을 수 있는 몇가지 측면과 모호함을 분명하게 해 줄 수 있었기를 바란다. 이후의 계획대로, 다음 챕터는 유효범위 체인(Scope chain), 식별자 처리( Identifier resolution) 그리고 계속해서 클로저(Closures)에 대해서 이야기하겠다.
 
 
추가 문헌(Additional literature)


 
10.1.3 – Variable Instantiation
10.1.5 – Global Object
10.1.6 – Activation Object
10.1.8 – Arguments Object
 


개발왕 김코딩

Howdy. Why so serious?

0개의 댓글

답글 남기기

아바타 플레이스홀더

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다