원문 출처 : ECMA-262-3 in detail. Chapter 5.Functions. by Dmitry Soshnikov
소개(Introduction)
이번 글에서는 ECMAscript의 객체 중에 하나인 함수(Function)를 알아보고, 함수에는 어떠한 것들이 있는지 설명한다. 함수가 콘텍스트의 변수 객체(Variables object)에 어떠한 영향을 미치며, 각 함수의 스코프 체인에는 무엇이 들어가는지도 알아볼 계획이다. 다음과 같은 질문을 자주 받는다.
첫 번째 함수,
var foo = function () { ... };
일반적인 방식으로 작성한 두 번째 함수.
function foo() { ... }
“두 함수는 뭐가 다르죠?”
또는 “왜 아래 함수는 괄호로 감싼 거예요?” 와 같은 질문을 받기도 한다.
(function () { ... })();
이 글은 이전 챕터와 관련있기 때문에, 아래에 나오는 내용을 제대로 이해를 하려면 Chapter 2. Variable object와 Chapter 4. Scope Chain을 먼저 읽는 게 좋다. 두 챕터에 나오는 용어를 적극적으로 사용할 생각이기 때문이다.
우선 어떤 함수들이 있는지 살펴보는 것을 시작으로 하나씩 차례차례 알아보자.
함수의 종류(Types of functions)
ECMAScript에는 세가지 종류의 함수가 있고, 각각은 고유한 특징을 갖는다.
함수 선언식(Function Declaration)
함수 선언식(줄여서 FD)은 다음과 같은 특징을 갖는 함수다.
- 반드시 이름을 갖는다.
- 소스 코드 위치에 자리한다. 프로그램 레벨이나 다른 함수의 몸체(FunctionBody) 안에 직접 위치한다.
- 콘텍스트 진입 시점에 생성한다.
- 변수 객체에 영향을 준다.
- 아래와 같이 선언한다.
function exampleFunc() { ... }
이 타입의 함수가 갖는 중요한 특징은 변수 객체에 영향을 미친다는 점이다(이 함수는 콘텍스트의 변수 객체에 들어간다). 이로 인해 함수를 코드 실행 시점에 이용할 수 있는데, 이 것이 두 번째 중요 포인트다(변수 객체의 본성에 기인하는 결과).
아래에 소스 코드 상에서 함수를 선언 전에 호출하는 예제가 나와있다.
foo(); function foo() { alert('foo'); }
소스 코드 내에 함수를 정의하는 위치 또한 중요하다(위에 함수 선언식을 정의한 내용 중 두 번째 항목을 참고).
// 함수를 다음 2가지 방법으로 선언할 수 있다. // 1) 전역 콘텍스트에 직접. function globalFD() { // 2) 또는 다른 함수의 몸체 내에 function innerFD() {} }
함수를 선언할 수 있는 위치는 코드 상에 두 군데가 있다(표현식이 들어가야 할 자리나 코드 블럭 내에는 선언할 수 없음).
함수 선언식 대신에 함수 표현식을 사용할 수도 있는데, 이번에는 함수 표현식에 대해서 알아보자.
함수 표현식(Function Expression)
함수 표현식(줄여서 FE)은 다음과 같은 함수다.
- 소스 코드의 표현식 위치에만 정의할 수 있다.
- 선택적으로 이름을 가질 수 있다.
- 함수 표현은 변수 객체에 영향을 주지 않는다.
- 코드 실행 시점에 생성한다.
항상 소스 코드 상의 표현식 자리에 위치한다는 것이 이 함수의 주요 특징이다. 간단한 할당 표현식 예제가 있다.
var foo = function () { ... };
이 예제는 어떻게 익명 함수 표현식(anonymouse FE)을 foo 변수에 할당하는지 보여준다. 할당이 끝나면 함수 이름인 foo를 호출할 수 있다.
또한 함수를 선언하는 구문에 선택적으로 이름을 줄 수 있다.
var foo = function _foo() { ... };
여기에서 주목해야 할 것은 함수 내부에서 _foo라는 이름을 사용할 수 있을 뿐만 아니라, FE의 바깥에서도 식별자 foo에 접근할 수 있다는 사실이다.
FE를 식별자에 할당하면 FD와 구분하기 어려워진다. 하지만 FE가 항상 표현식에 위치한다는 사실을 알고 있다면, 둘을 쉽게 구분할 수 있다. 다음 예제에는 다양한 ECMAScript 표현식이 나와있는데, 모든 함수는 FE다.
// 괄호(그룹화 연산자) 안에서는 표현식이 된다. (function foo() {}); // 배열 리터럴 안에 있을 경우에도 표현식이다. [function bar() {}]; // 콤마 또한 표현식으로 처리한다. 1, function baz() {};
이런 함수가 필요하긴 할까? 대답은 분명하다. 표현식 위치에서 함수를 사용하고 변수 객체를 오염시키지 않으려면 필요하다. 이러한 사실은 함수를 다른 함수의 인자로 전달하는 걸로 증명할 수 있다.
function foo(callback) { callback(); } foo(function bar() { alert('foo.bar'); }); foo(function baz() { alert('foo.baz'); });
FE를 변수에 할당하면, 함수는 메모리에 계속 존재한다. 따라서 나중에 변수명으로 접근할 수 있다(알고 있듯이 변수가 변수 객체(VO)에 영향을 주기 때문).
var foo = function () { alert('foo'); }; foo();
보조적인 역할을 하는 도우미 데이터를 외부 콘텍스트에 감추기 위해서 유효범위를 캡슐화하는 예제가 있다(FE를 생성 직후 호출).
var foo = {}; (function initialize() { var x = 10; foo.bar = function () { alert(x); }; })(); foo.bar(); // 10; alert(x); // "x" is not defined
함수 foo.bar(foo의 [[Scope]] 프로퍼티에 있는)는 initialize 함수의 내부에 있는 변수 x에 접근할 수 있다. 그리고 x는 외부에서직접 접근할 수 없다. 많은 라이브러리가 “private” 데이터를 만들어서 보조 개체를 감추는 데 이 전략을 이용한다. 초기화하는 FE의 이름을 종종 생략하기도 한다.
(function () { // 초기화 스코프 })();
런타임에 조건에 따라 FE를 생성함으로써 VO를 오염시키지 않는 예제도 있다.
var foo = 10; var bar = (foo % 2 == 0 ? function () { alert(0); } : function () { alert(1); } ); bar(); // 0
ES5는 함수 바인딩을 표준화했다. 이 함수는 this 값을 바인딩하여, 다른 함수 호출 시에 this 값이 변경되지 않게 잠근다.
var boundFn = function () { return this.x; }.bind({x: 10}); boundFn(); // 10 boundFn.call({x: 20}); // still 10바인딩한 함수는 지연(setTimeout) 함수나 이벤트 리스너에 자신의 this 값으로 어떤 객체를 처리해야하는 함수를 붙일 때 가장 많이 사용한다.
감싸는 괄호에 대한 질문(Question “about surrounding parentheses”)
이 글의 처음으로 다시 돌아가서 “왜 괄호로 함수를 감싸야만 선언과 동시에 호출할 수 있지?” 라는 질문에 답을 해야겠다. 이유는 표현식 구문이 갖는 제약 때문이다.
표준에 따라서, 표현식 구문은(ExpressionStatement) 여는 중괄호, {로 시작할 수 없다. 블럭과 구분할 수 없기 때문이다. 그리고 함수 선언과 구분하기 힘들기 때문에 함수 키워드로 시작해서도 안 된다. 다시 말해서, 즉시 실행 함수(function 키워드로 시작하는)를 만들기 위해서 아래와 같이 함수 선언식을 작성했다면,
function () { ... }(); // 또는 아래와 같이 이름이 있는. function foo() { ... }();
두 경우 모두 파서가 해석 에러를 보고할 것이다. 이 에러의 원인은 다양하다.
전역 코드에 이렇게 선언을 하면(즉, 프로그램 레벨에), function 키워드로 시작하기 때문에 파서는 코드를 함수 선언식으로 이해한다. 그런데 첫번째 경우는 함수의 이름이 없기 때문에 SyntaxError를 보고한다.
두 번째의 경우는 함수에 이름(foo)이 존재하기 때문에 파서가 정상적인 함수 선언으로 처리한다. 하지만 내부에 표현식이 없는 그룹화 연산자를 사용하고 있음을 알리는 문법 에러가 발생한다. 이 경우에 함수 선언 뒤에 오는 것은 함수 호출을 위한 괄호가 아니라 그룹화 연산자일 뿐이다. 만약 코드를 다음과 같이 작성했다면,
// "foo"는 함수 선언이다 // 그리고 실행 콘텍스트 진입 시점에 생성한다. alert(foo); // function function foo(x) { alert(x); }(1); // 이것은 호출이 아니라, 그룹화 연산자다. foo(10); // 10
함수 선언과 표현식 (1)을 가지고 있는 그룹화 연산자가 있기 때문에 두 구문 모두 아무런 문제가 없다. 위의 예제는 아래의 예제와 같다.
// 함수 선언 function foo(x) { alert(x); } // 표현식이 있는 그룹화 연산자 (1); // 다른 (function) 표현식을 갖는 또 다른 그룹화 연산자 (function () {}); // 내부에 있는 표현식 ("foo"); // etc
구문 안에 이와 같은 정의를 한 경우에는 모호함 때문에 문법 에러가 발생한다.
if (true) function foo() {alert(1)}
ECMA 스펙상으로 볼 때, 위의 코드는 잘못된 구문이다(표현식 구문은 function 키워드로 시작할 수 없다). 하지만 아래에 나와있는 것처럼, 문법 에러를 제공하는 ECMAScript 구현체는 하나도 없으며 모두 이를 각자 나름의 방식으로 처리한다.
지금까지 설명한 내용을 가지고, 어떻게 파서에게 함수를 생성과 동시에 실행하고 싶다고 이야기할 수 있을까? 방법은 쉽다. 함수 선언식이 아닌 함수 표현식을 사용하면 된다. 표현식을 만드는 가장 간단한 방법은 위에서 이야기 했듯이 그룹화 연산자를 사용한다. 그룹화 연산자 안에 표현식을 두면, 파서는 함수 표현식(FE)인 코드를 구분할 수 있으며 이에 따라 모호함도 사라진다. 이러한 함수는 코드 실행 단계 동안에 만들어지고, 함수 실행이 끝난 후에는 사라진다(함수를 참조하고 있는 곳이 없다면).
(function foo(x) { alert(x); })(1); // 이건 그룹화 연산자가 아닌 함수 호출이다.
예제의 마지막에 있는 괄호는 FD의 경우처럼 그룹화 연산자가 아니라 함수 호출 괄호다.
다음 예제에 나오는 즉시 호출 함수는 괄호로 감쌀 필요가 없다는 것에 주목하자. 이유는 함수가 표현식의 위치에 있어서 파서가 이를 코드 실행 시점에 생성하는 FE로 처리해야 한다는 것을 이미 알고 있기 떄문이다.
var foo = { bar: function (x) { return x % 2 != 0 ? 'yes' : 'no'; }(1) }; alert(foo.bar); // 'yes'
얼핏보면 foo.bar는 함수가 아니라 문자열처럼 보인다. 여기에 있는 함수는 프로퍼티를 초기화 할 때만 사용하는데, 조건 매개변수 값에 따라서 값을 돌려주는 함수를 만들고 바로 실행한다.
따라서, “괄호”를 묻는 질문에 완벽한 대답은 다음과 같다.
그룹화 괄호는 함수가 표현식의 위치에 있지 않을 때 필요하고, 함수를 생성 후 즉시 실행하고 싶은 경우에는 직접 함수를 FE로 변환한다.
파서가 FE로 처리해야 한다는 것을 아는 경우, 즉 함수가 이미 표현식의 위치에 있는 경우에는 괄호가 필요없다.
괄호를 감싸는 방법 외에 함수를 FE 타입으로 변경할 수 있는 다른 방법이 있다. 예를 들어,
1, function () { alert('익명함수를 호출합니다.'); }(); // 또는 이렇게, !function () { alert('ECMAScript'); }(); // 그리고 수동적으로 변경하는 다른 방법들 ...
하지만, 그룹화 괄호를 사용하는 것이 가장 많이 쓰이고 우아하게 처리할 수 있는 방법이다.
그룹화 연산자는 호출 괄호 없이도 함수 디스크립션을 감쌀 수 있다. 아래에 있는 표현식은 둘 다 올바르다.
(function () {})(); (function () {}());
구현의 확장 : Function문
다음에 나오는 예제 코드는 어떤 ECMAScript 구현체도 명세를 따르지 않았음을 보여준다.
if (true) { function foo() { alert(0); } } else { function foo() { alert(1); } } foo(); // 1 또는 0? 다른 ECMAScript 엔진에서 테스트 해보자.
표준에 비춰볼 때 이 구조는 문제가 있다. 코드 블럭 안에 함수 선언식(FD)을 둘 수 없기 때문이다(지금은 if와 else가 FD를 가지고 있음). 위에서 이야기 했듯이, FD는 프로그램 레벨이나 다른 함수의 몸체 안에 직접 위치해야 한다.
코드 블럭은 오직 구문만 가질 수 있기 때문에 위의 예제는 잘못되었다. 블럭 내에 함수는 표현식의 위치에만 나올 수 있으며, 함수를 정의할 때는 여는 중괄호(코드 블럭과 구분할 수 없음)나 함수 키워드로 시작할 수 없다(FD와 구분할 수 없음).
하지만 표준 문서의 error processing 섹션은 ECMAScript 구현체가 프로그램 구문을 확장할 수 있도록 허용하고 있다. 그리고 블럭 안에 등장하는 함수 처리가 이러한 확장 중에 하나다. 오늘날 존재하는 모든 구현체는 이 경우에 예외를 던지지 않고 각자 고유의 방식으로 처리한다.
위 예제의 if-else 분기문은 두 함수 중 어떤 것을 정의할지 선택할 수 있다고 가정한다. 이 결정은 런타임에 이루어지기 때문에, 함수 표현식(FE)을 사용해야 한다. 하지만 대부분의 구현체는 단순하게 콘텍스트 진입 시점에 두 개의 함수 선언식(FD)을 모두 생성한다. 두 함수 모두 같은 이름을 사용하기 때문에, 마지막에 선언한 함수만 호출할 수 있다. 이런 이유로 이 예제를 실행하면 else로 코드 제어가 이동할 수 없음에도 불구하고 foo 함수는 1을 출력한다.
SpiderMonkey 구현체는 이 경우를 두 가지 방식으로 처리한다. 한 편으로는 이러한 함수를 선언식으로 취급하지 않는다(코드 실행 시점에 조건문에서 함수를 생성한다). 하지만 다른 한편으로는 진짜 함수 표현식은 아니다. 괄호로 감싸지 않고 호출할 수 없으며(FD와 구분할 수 없기 떄문에 해석 에러가 발생) 이 함수를 변수 객체에 저장하기 때문이다.
나는 SpiderMonkey가 이 경우를 중간 형태의 함수(FE + FD)로 분리해서 잘 처리하고 있다고 생각한다. 조건에 따라서 이러한 함수를 정확하게 생성하고는 있지만, 이 함수는 FE 보다는 외부에서 호출할 수 있는 FD에 더 가깝다. SpiderMoneky는 이러한 구문 확장에 함수 구문(Function Statement, 줄여서 FS)이라는 이름을 붙였다. 이 용어는 MDC(Mozilla Developer Center)에서 언급하고 있다. 또한 JavaScript를 만든 브렌든 아이히도 SpiderMoneky가 이러한 타입의 함수를 제공하고 있다는 사실을 알고 있다.
기명함수 표현식의 특징(Named Function Expression, NFE)
이름을 갖는 FE(기명 함수 표현식, 줄여서 NFE)는 중요한 특징 하나를 가지고 있다. 함수 표현식을 정의할 때 이야기 했던 것처럼(그리고 위의 예제에도 나왔음) 함수 표현식은 콘텍스트의 변수 객체에 영향을 주지 않는다(선언 하기 전이나 선언하고 난 후에 이름으로 함수를 호출할 수 없다는 것을 의미) . 하지만 FE는 이름으로 자기 자신을 재귀 호출할 수 있다.
(function foo(bar) { if (bar) { return; } foo(true); // "foo" 이름을 이용할 수 있다. })(); // 하지만 외부에서는 "foo"를 이용할 수 없다. foo(); // "foo" is not defined
“foo”를 어디에 보관하는 걸까? foo의 활성화 객체 안에? 아니다, foo 함수 내부에서 “foo”라는 이름을 정의한 적이 없다. 그렇다면 foo를 생성하는 콘텍스트의 변수객체 안에? 역시 아니다. FE는 VO에 영향을 주지 않는다는 사실을 외부에서 foo를 호출하면서 확인했다. 그렇다면 어디일까?
이런 방식으로 동작한다. 코드 실행 시점에 인터프리터가 기명 함수 표현식(NFE)을 만나면. 함수 표현식을 만들기 전에 보조 특수 객체(auxiliary specilal object)를 만들고 스코프 체인의 가장 앞에 이 특수 객체를 추가한다. 그런 다음에 함수 표현식을 만드는데, 이 때 함수에 [[Scope]] 프로퍼티(Chapter 4. Scope chain에서 배웠듯이)가 생긴다. 여기에는 함수를 생성하는 콘텍스트의 스코프 체인이 들어있다(즉, [[Scope]] 안에 특수 객체가 위치한다). 다음으로, 기명 함수 표현식을 특수 객체에 고유 프로퍼티로 추가한다. 이 프로퍼티의 값은 함수 표현식을 참조한다. 그리고 마지막으로 부모의 스코프 체인에서 특수 객체를 제거한다. 이 알고리즘을 수도코드로 알아보자.
specialObject = {}; Scope = specialObject + Scope; foo = new FunctionExpression; foo.[[Scope]] = Scope; specialObject.foo = foo; // {DontDelete}, {ReadOnly} delete Scope[0]; // 스코프 체인의 가장 앞에 있는 specialObject를 삭제한다.
따라서, 외부에서는 이 함수의 이름을 사용할 수 없다. 함수의 [[Scope]] 안에 특수 객체가 저장되어 있기 때문에, 내부에서는 이 함수의 이름을 사용할 수 있다.
하지만 Rhino 같은 일부 구현체는 이 선택적으로 부여할 수 있는 함수의 이름을 특수 객체가 아닌 함수 표현식의 활성화 객체에 저장한다는 사실을 주의해야 한다. Microsoft의 구현체인 JScript는 이 규칙을 완전히 위반해서 함수의 이름을 부모 변수 객체에 저장하기 때문에 외부에서도 함수에 접근할 수 있다.
NFE와 SpiderMonkey
다른 구현체는 이 문제를 어떻게 처리하는지 살펴보자. SpiderMonkey 일부 버전은 버그라고 할 수 있는, 특수 객체와 관련한 한 가지 특징을 가지고 있다(비록 표준에 따라서 구현했지만 명세서 편집상의 결함에 더 가깝다). 이는 식별자 처리 메커니즘과 관련 있다. 2차원 스코프 체인 분석 과정에서 식별자를 해석 할 때, 구현체는 스코프 체인 내에 있는 모든 프로토타입 체인을 검색한다.
Object.prototype에 프로퍼티를 정의한 후 코드에 존재하지 않는 변수를 사용해보면 이 메커니즘을 확인할 수 있다. 다음에 나오는 예제에서 x를 해석할 때, x를 찾지 못하고 결국 전역 객체에 도달한다. 하지만 SpiderMonkey의 전역 객체는 Object.prototype을 상속하기 때문에 Object에서 x를 발견하고 해석한다.
Object.prototype.x = 10; (function () { alert(x); // 10 })();
활성화 객체는 프로토타입이 없다. 시작 조건이 같을 경우, 아래 예제의 중첩 함수는 동일한 동작을 수행한다. 지역 변수 x를 정의하고 중첩 함수(FD 또는 익명 FE)를 선언한 다음에 중첩함수에서 x를 참조하면, 이 변수는 보통 Object.prototype이 아닌 부모 함수의 콘텍스트(변수가 있어야 하는, 그리고 있는 곳)에서 해석된다.
Object.prototype.x = 10; function foo() { var x = 20; // 함수 선언 function bar() { alert(x); } bar(); // 20, AO(foo) // 같은 익명 함수 표현식 (function () { alert(x); // 20, 역시 AO(foo) })(); } foo();
활성화 객체에 prototype을 설정하는 일부 예외적인 구현체가 있다. Blackberry 구현체는 위의 예제에 나오는 x 값을 10으로 해석한다. Object.prototype에서 값을 찾기 때문에 foo의 활성화 객체까지 검색하러 가지 않는다.
AO(bar FD or anonymous FE) -> no -> AO(bar FD or anonymous FE).[[Prototype]] -> yes - 10
그리고 구형 버전의 SpiderMoneky(ES5를 지원하지 않는)가 기명 함수 표현식(NFE)의 특수 객체를 처리하는 방식도 이와 비슷하다. 이 특수 객체는 표준을 따르는 일반적인 객체로 new Object()로 생성한 것과 같다. SpiderMonkey 구형 버전(1.7 버전까지)의 특수 객체는 Object.prototype을 상속한다. 새로운 버전의 SpiderMonkey는 특수 객체에 prototype을 설정하지 않는다.
function foo() { var x = 10; (function bar() { alert(x); // 10이 아닌 20. AO(foo)에 도착하지 못한다. // "x"를 체인 안에서 처리한다. // AO(bar) - no -> __specialObject(bar) -> no // __specialObject(bar).[[Prototype]] - yes: 20 })(); } Object.prototype.x = 20; foo();
ES5에서는 이런 동작이 변경되었음을 주의한다. Firefox 현재 버전은 함수 표현식의 이름을 Object.prototype을 상속하지 않는 environment에 저장한다.
NFE와 JScript
Microsoft의 ECMAScript 구현체이자 현재 Internet Explorer에 내장되어 있는 JScript는 기명 함수 표현식(NFE)과 관련한 많은 버그를 가지고 있다. 이러한 버그는 모두 ECMA-262-3 표준에 완전히 모순된다. 일부 버그는 심각한 에러를 만들 수도 있다.
첫째, 이 경우에 JScript는 함수의 이름을 변수 객체에 저장하지 않아야 한다는 FE의 핵심 규칙을 어긴다. 특수 객체에 저장하고 오직 함수 자신 내부에서만 접근할 수 있어야 하는 선택적인 FE의 이름을 직접 부모 변수 객체에 저장한다. 게다가, JScript는 기명 함수 표현식(NFE)을 FD로 처리한다. 즉, 콘텍스트 진입 시점에 함수를 생성하기 때문에 호이스팅 대상이다.
// FE는 FD처럼 선언이 나오기 이전에 변수 객체에서 FE의 이름을 사용할 수 있다. testNFE(); (function testNFE() { alert('testNFE'); }); // 그리고 FE의 이름이 변수 객체에 있기 때문에 FD처럼 선언 이후에도 사용할 수 있다. testNFE();
예제에 나와있는 것처럼 완벽하게 규칙을 위반한다.
둘째, 기명 함수 표현식을 정의하면서 변수에 할당 하는 경우에 JScript는 두 개의 다른 함수 객체를 생성한다. 이러한 동작에 논리적인 이름을 붙이기가 어렵다(특히 기명 함수 표현식의 이름을 외부에서 전혀 접근할 수 없다는 것을 생각해보면).
var foo = function bar() { alert('foo'); }; alert(typeof bar); // "function", NFE가 VO 안에 있다. – 이미 이건 실수다. // 게다가 더 흥미로운 것은 둘이 다른 존재라는 것. alert(foo === bar); // false! foo.x = 10; alert(bar.x); // undefined // 하지만 두 함수 모두 같은 동작을 한다. foo(); // "foo" bar(); // "foo"
상황이 좀 어지럽다.
변수 할당과는 별개로 기명 함수 표현식(NFE)을 설명하려면, 변수에 함수를 할당한 후에 마치 하나의 객체인 것처럼 동등성 비교 결과로 true를 반환하는지 확인해야 한다.
(function bar() {}); var foo = bar; alert(foo === bar); // true foo.x = 10; alert(bar.x); // 10
이 상황을 설명해보면, 실제로는 두 개의 객체를 만들었다. 하지만 마지막까지 남는 것은 단 하나다. 예제에 있는 기명 함수 표현식을 함수 선언(FD)으로 처리하고 난 후에 콘텍스트 진입 시점에 함수 선언식 bar를 생성한다. 코드 실행 시점에는 이미 두 개의 객체가 있다. 함수 표현식(FE) bar를 생성하고 어디에도 저장하지 않는다. 함수 표현식 bar를 참조하는 곳이 없기 때문에 bar는 제거된다. 그 결과 foo 변수에 할당하여 참조가 남아있는 함수 선언식 bar만 남는다.
셋째, arguments.callee를 통해서 간접적으로 현재 실행중인 함수의 이름을 갖는 객체를 참조할 수 있다(두 개의 함수 중 현재 실행중인 함수).
var foo = function bar() { alert([ arguments.callee === foo, arguments.callee === bar ]); }; foo(); // [true, false] bar(); // [false, true]
넷째, JScript는 기명 함수 표현식(NFE)을 일반적인 함수 선언식(FD) 처럼 처리하기 때문에, 함수 선언식은 조건 연산 규칙의 적용을 받지 않는다. 다시 말해서, 기명 함수 표현식(NFE)을 함수 선언식(FD)처럼 콘텍스트 진입 시점에 생성하기 때문에 코드 내에 마지막으로 정의한 함수를 사용한다.
var foo = function bar() { alert(1); }; if (false) { foo = function bar() { alert(2); }; } bar(); // 2 foo(); // 1
이 동작을 “논리적”으로 설명할 수 있다. 콘텍스트에 진입할 때 마지막에 만난 bar라는 이름을 가진 함수 선언식(FD)를 만든다. 이 함수는 alert(2)를 실행 한다. 그 다음에 코드를 실행할 때는 foo 변수에 참조를 할당한 새로운 함수 표현식(FE) bar를 생성한다. 그 결과(조건이 false인 if-block에 도달할 수는 없음), foo를 실행하면 alert(1)을 실행한다. 로직은 분명하지만 IE의 버그라는 점을 감안해서 “논리적”이라는 단어를 사용했다. 명백한 오작동이고 JScript의 버그이기 때문이다.
다섯째, JScript의 기명 함수 표현식 버그는 비자격 식별자(var 키워드를 생략해서 만든 전역 프로퍼티)에 값을 할당해서 전역 객체의 프로퍼티를 생성하는 것과 관련있다. 여기에서는 기명 함수 표현식(NFE)을 함수 표현식(FE)으로 처리하기 때문에, 함수 표현식(FE)을 변수 객체에 저장하고 비자격 식별자에 할당한다(변수가 아니라 전역 객체의 프로퍼티임). 이 경우에 함수의 이름이 비자격 식별자와 같다면, 이 프로퍼티는 전역 프로퍼티에 등록되지 못한다.
(function () { // var를 생략하면 지역 콘텍스트의 변수가 아닌, 전역 객체의 프로퍼티가 된다. foo = function foo() {}; })(); // 하지만 익명 함수의 밖에서 foo 식별자를 사용할 수 없다. alert(typeof foo); // undefined
다시 한 번 말하지만, “로직”은 분명하다. 함수 선언식 foo는 콘텍스트에 진입할 때 익명함수의 지역 콘텍스트를 반영하는 활성화 객체를 갖는다. 그리고 코드 실행 시점에 foo라는 이름이 이미 AO에 존재하기 때문에 이를 지역적으로 처리한다. 결과적으로, ECMA-262-3에 따라서 할당 연산을 수행할 때 AO에 존재하고 있는 프로퍼티 foo를 갱신할 뿐, 전역 객체에 새로운 프로퍼티를 만들지는 않는다.
함수 생성자를 이용하여 만든 함수
이 유형의 함수 객체는 고유의 특징을 갖고 있기 때문에 FD, FE와 분리해서 알아본다. 이 함수의 [[Scope]]가 오로지 전역 객체만을 갖는다는 점이 주요 특징이다.
var x = 10; function foo() { var x = 20; var y = 30; var bar = new Function('alert(x); alert(y);'); bar(); // 10, "y" is not defined }
bar 함수의 [[Scope]]는 foo 콘텍스트의 AO를 갖지 않는다. 변수 y에 접근할 수 없으며, 변수 x는 전역 콘텍스트에서 가져온다. 그런데 Function 생성자는 new 키워드와 함께 사용할 수도 있고, new 키워드 없이도 사용할 수 있다는 점을 주목한다. 둘의 차이는 동등하다.
다른 특징은 “Equated Grammar Productions and Joined Objects“와 관련있다. ECMA 명세서는 최적화를 위해 이 메커니즘을 제안한다(하지만, 구현체는 이러한 최적화를 반드시 사용해야 하는 것은 아니다). 예를 들어, 길이가 100인 배열에 함수를 채워넣는 루프가 있다고 생각해보자. 구현체는 반복 할당 작업 시에 결합 객체 메커니즘을 이용하여 오직 하나의 함수 객체만 사용할 수 있다.
var a = []; for (var k = 0; k < 100; k++) { a[k] = function () {}; // 결합 객체를 사용할 것이다. }
하지만 Function 생성자로 만든 함수는 결합하지 못한다.
var a = []; for (var k = 0; k < 100; k++) { a[k] = Function(''); // 새로운 함수 100개를 할당한다. }
다음은 결합 객체와 관련있는 또 다른 예제다.
function foo() { function bar(z) { return z * z; } return bar; } var x = foo(); var y = foo();
여기에서도 함수를 물리적(내부의 [[Scope]] 프로퍼티도 포함)으로 같은 대상으로 볼 수 있기 때문에 객체 x와 y를 결합할 수 있다(하나의 객체를 사용하기 위해서). 결론적으로 Function 생성자를 이용해서 생성한 함수는 항상 더 많은 메모리 자원을 더 많이 차지한다.
함수 생성 알고리즘
함수의 의사 코드 생성 알고리즘(객체를 결합하는 단계는 제외)이 아래에 나와있다. ECMAScript의 함수 객체를 좀 더 자세하게 이해하는 데 여기에 있는 설명이 도움이 될 것이다. 이 알고리즘은 모든 종류의 함수에 동일하게 적용된다.
F = new NativeObject(); // property [[Class]]는 "Function" F.[[Class]] = "Function" // 함수 객체의 prototype F.[[Prototype]] = Function.prototype // 함수 자신에 대한 참조 // [[Call]] 는 표현식 F()를 호출해서 활성화한다. // 그리고 새로운 실행 콘텍스트를 만든다. F.[[Call]] = <reference to function> // 일반적인 객체 생성자를 만든다. // [[Construct]]는 "new" 키워드로 활성화한다. // 그리고 메모리에 새로운 객체를 할당한다. // 그 다음에 F.[[Call]]을 호출해서 객체를 초기화하고 // 새로 만든 객체의 this 값에 전달한다. F.[[Construct]] = internalConstructor // 현재 콘텍스트의 스코프 체인 // 즉, 함수 F를 생성하는 콘텍스트 F.[[Scope]] = activeContext.Scope // if this functions is created // via new Function(...), then F.[[Scope]] = globalContext.Scope // 형식 매개변수의 개수 F.length = countParameters // F 객체가 생성한 prototype __objectPrototype = new Object(); __objectPrototype.constructor = F // {DontEnum}, 반복문 안에서 열거할 수 없다. F.prototype = __objectPrototype return F
F.[[Prototype]]은 함수(생성자)의 prototype이고, F.prototype은 이 함수가 만든 객체의 프로토타입이라는 사실에 명심하자(종종 용어에 혼란이 있기 때문이고 어떤 글은 F.prototype을 “생성자 prototype”이라고 하는데 이는 잘못되었다).
결론(Conclusion)
이번 글에서는 큰 사실을 밝혔다. 하지만, 객체와 prototype를 다루는 챕터 중에 하나에서 생성자로서 함수가 어떻게 동작하는지를 이야기할 때 함수를 다시 언급할 생각이다. 언제나 그렇듯이, 궁금한 점은 댓글로 남겨주면 감사하겠다.
추가 문헌(Additional literature)
13. – Function Definition;
15.3 – Function Objects.
0개의 댓글