저번 글을 통해 함수가 어떻게 생성되는지 대략적으로 살펴보았다.
this
참조 방식을 결정한다.이제 ECMAScript가 실제로 이 함수 객체를 함수로 호출할 때 어떤식으로 동작하는지 찾아보자. 이 글에서는 제너레이터 함수나 async 함수 등 조금은 특별한 함수보다는 일반적인 함수와 일반적인 호출에 대해서만 알아보려 한다.
Call(F, V, [, argumentsList])
ECMAScript 2017은 함수 호출을 Call(F, V, [, argumentsList])
으로 표현한다. Call
은 함수 객체의 내부 [[Call]]
메서드를 수행하는 동작(Abstract operation)으로 F, V, argumentsList
를 인자로 받는다. F
는 함수 객체, V
는 [[Call]]
의 this
값, argumentsList
는 함수 호출 시 전달한 인자로 기본값은 빈 리스트이다.
argumentsList
가 전달되지 않았으면 빈 리스트로 지정한다.F
가 Callable
이 아니면 에러를 발생시킨다.F.[[Call]](V, argumentsList)
수행 결과를 반환한다.3번에서 알 수 있듯 실질적인 함수의 호출과 그 동작은 F.[[Call]]
에 나타나 있다. 이제 F.[[Call]]
을 알아보자.
참고로 ECMAScript에서 함수를 호출하는 동작을 위와 같이 표현한 것일 뿐, 실제 자바스크립트 엔진이 ECMAScript2017의 Call
이라는 연산을 위와 똑같이 정의한 것은 아니다.
F.[[Call]](thisArgument, argumentsList)
지난 글에서 설명했듯 [[Call]]
은 함수 객체의 내부 메서드(Internal method)다. 인자로 this
값과 argumentsList
를 받는다.
F.[[FunctionKind]]
가 "classConstructor"라면 에러를 발생시킨다.callerContext
는 현재 실행 중인 실행 컨텍스트(running execution context)calleeContext
에 새로운 실행 컨텍스트를 생성하여 지정한다. (PrepareForOrdinaryCall)calleeContext
가 현재 running execution context이다.)this
를 바인딩한다. (OrdinaryCallBindThis)result
에 그 결과를 저장한다. (OrdinaryCallEvaluateBody)calleeContext
를 execution context stack에서 제거하고, callerContext
를 다시 running execution context로 지정한다.result
를 반환한다.위 동작에서 Execution Context, execution context stack, running exeuction context 내용이 조금 난해할 수 있을 것 같다. 우선은 이런 게 있다 정도로만 생각하고, 앞으로 차근차근 알아가 보도록 하자.
함수 호출을 조금 더 쉽게 정리해보면 다음과 같다.
this
를 바인딩한다. (this
가 어떤 객체를 참조해야 할지 결정한다.)result
에 저장한다.result
를 반환한다.Execution Context(이하 EC)는 스코프(식별자 이름과 값의 매칭)와 기본 객체들(intrinsic objects - Array, Object 등의 기본 생성자와 그 프로토타입 등)을 가지고 있는 Realm 등 코드 수행 환경에 대한 여러 정보를 가지고 있는 어떤 장치라고 생각하면 된다. 결국, EC는 ECMAScript에서 코드 수행(evaluation) 매커니즘을 표현하기 위한 것이며, 실제 스크립트 엔진들은 이 명세와 완벽히 일치하지 않을 수 있다.
자바스크립트가 단일 쓰레드 환경으로 코드를 컴파일하고 실행할 때 call stack을 만드는 것과 같이 EC Stack을 만든다고 생각해보자. 가장 밑바탕에는 Global 코드 환경에 대한 EC가 있을 것이고, 그 위로 함수가 호출될 때마다 그에 맞는 EC가 하나씩 추가되고 빠지기를 반복할 것이다. 그리고 각 시점에 stack의 최상위에 있는 EC가 바로 running execution context인 것이다.
다음과 같은 코드에서 EC stack이 어떻게 변하는지 살펴보자.
// global
function foo() {
function bar() {
return "bar";
}
return bar();
}
foo();
|--------|
| bar |
|--------| |--------| |--------|
| foo | | foo | | foo |
|--------| |--------| |--------| |--------| |--------|
| global | | global | | global | | global | | global |
|--------| |--------| |--------| |--------| |--------|
ECMAScript 코드 수행을 위한 EC에는 LexicalEnvironment와 VariableEnvrironment라는 컴포넌트가 존재한다. 간단히 변수의 참조를 기록하는 환경이라고 생각하면 된다. LexicalEnvironment와 VariableEnvironment가 서로 나누어져 있지만, 사실은 초기화 시에 같은 객체를 바라보고 있다. with
와 같은 특별한 문장을 만나면 그 block 내부에서는 새로 만들어진 LexicalEnvironment를 참조한다.
(얼핏 살펴본 바로는 기본값 parameter도 Lexical/VariableEnvironment와 관련이 있다. 앞으로 이어질 3편 글에서 Environment, Environment Record 그리고 this에 대해 조금 더 자세히 알아볼 예정이다.)
위에서 알아본 [[Call]]
의 동작에서 OrdinaryCall 이라는 단어가 3번 등장했다.
위 3개는 ECMAScript에서 정의하고 있는 내부 동작인데 하나씩 살펴볼 필요가 있을 것 같다.
PrepareForOrdinayCall은 결국 EC를 만들고 초기화 시키는 내용을 가진 동작을 추상적으로 표현한것인데 조금 더 자세히 보면 아래 3단계를 갖는다. (Realm은 이 글에서는 큰 관련이 없어 설명에서 제외하였다.)
calleeContext
)calleeContext
들어갈 Lexical Environment를 생성한다.calleeContext
를 EC Stack에 추가(push)한다. 따라서 calleeContext
가 running execution context가 된다.OrdinayCallBindThis는 함수 객체의 [[ThisMode]]
에 따른 this
값 참조를 결정한다. 화살표 함수(Arrow Function), Strict mode, Environment와 연결된다.
[[ThisMode]]
가 lexical인 경우는 다른 처리를 하지 않는다. (arrow function)[[ThisMode]]
가 strict인 경우 인자로 넘어온 thisArgument를 Environment record에 설정한다.[[ThisMode]]
가 lexical도 아니고 strict도 아닌 경우 global에 있는 [[thisValue]]
를 Environment record에 설정한다.OrdinaryCallEvaluateBody는 다음 두개 동작으로 나눌 수 있다.
변수 선언 초기화는 결국 Environment Record(식별자 이름과 값의 매칭을 위한 표 정도로 이해할 수 있을 것 같다)를 채우는 동작인데, 꽤 복잡한 동작들이 작성돼 있다. 특히 arguments 객체가 필요한지 아닌지, 기본값 매개변수가 있는지 없는지에 따라 분기가 나뉘는데 결국에는 또 EC의 LexicalEnvironment와 VariableEnvironment로 이어지게 된다.
결국, 함수의 호출을 이해하기 위해서는 EC와 EC 내부의 LexicalEnvironment, VariableEnvironment를 이해해야 한다. 함수 한번 호출하는데 참 많은 내용을 알아야 하면서도 이렇게까지 알아야 하나 싶기도 하는 마음도 들기 시작했다. 하지만 그래도 ECMAScript의 함수를 이해하고 코드를 작성하는 것은 자유도가 높은 자바스크립트에서 예기치 않은 버그를 방지할 수 있는 데 많은 도움이 될 것 같다.
그리고 앞으로 이어질 글에서 Environment와 Record, 변수 선언 초기화 등에 대해 더 자세히 알아보려 한다. 잠시 예고를 하자면, 아래에 있는 JS 코드가 어떻게 수행되고 또 왜 에러가 발생하는지 그 이유를 알 수 있을 것이다.
function add(a, b) {
return a + b;
}
function foo(a, b = add(a, 1)) {
return `foo ${a + b}`;
}
function bar(a = add(b, 1), b) {
return `bar ${a + b}`;
}
console.log(foo(1)); // foo 3
console.log(bar(undefined, 1)); // Error