이번 포스팅에선 자바스크립트 실행 환경에 대해서 알아보겠다.
자바스크립트에서 말하는 실행 컨텍스트란, 실행 가능한 코드가 실행되기 위해 필요한 환경으로 이해할 수 있다.
실행 환경(Execution Context)는 다음과 같은 프로퍼티를 갖는다.
Variable Environment 세 가지 프로퍼티를 가지고 있다.
바로 environment record
와 outer environment reference
그리고 ThisBinding
이다.
this
의 값이 여기서 결정된다. 글로벌 실행 컨텍스트에서 this
는 global object 이다.
함수 실행 컨텍스트에서는 this
값은 어떻게 함수가 호출되었는지에 따라 달라진다. 만약 함수가 object reference로 호출되었다면 this
는 해당 객체를 가리키게 된다. 그렇지 않으면 this
는 글로벌 객체(window)를 가리키거나 strict mode에서는 undefined
를 가리키고 있다.
그럼 outer environment가 어떤 형태로 존재하여 상위 스코프를 탐색할 수 있는지 살펴보겠다.
// global
const globalA = 'globalA';
function foo() {
const fooA = 'fooA';
function bar() {
const barA = 'barA';
console.log(globalA); // globalA
console.log(fooA); // fooA
console.log(barA); // barA
console.log(unknownA); // Reference Error
}
bar();
}
foo();
위의 예제 코드가 있고, 각 코드의 실행 환경에 따른 environment를 살펴보면 다음과 같다.
GlobalEnvironment = {
// Global Environment Record에는
// Object Environment Record와 Declarative Environment Record 등이 같이 존재하지만 이 글에서는 구분하지 않겠다.
environmentRecord: {
globalA: 'globalA'
},
outer: null
ThisBinding: <Global Object>
};
fooEnvironment = {
environmentRecord: {
fooA: 'fooA'
},
outer: globalEnvironment // foo는 Global에서 생성됐다.
ThisBinding: <Global Object>
}
barEnvironment = {
environmentRecord: {
barA: 'barA'
},
outer: fooEnvironment // bar는 foo 안에서 생성됐다.
ThisBinding: <Global Object>
}
bar의 environment에서는 fooA
나 globalA
를 찾을 수 없기때문에 outer참조를 통해 상위 environment로 올라가 식별자를 찾아간다. outer
가 null
임에도 불구하고 unknownA
처럼 찾을 수 없는 식별자라면 Reference Error가 발생한다.
Lexical Environment는 Variable Environment와 동일하게 environment record
와 outer environment reference
그리고 ThisBinding
를 가지고 있다.
Lexical Environment와 Variable Environment의 차이점은 다음과 같다.
위의 내용으로는 이해가 잘 안될 수 있다. 코드를 한 번 살펴보겠다.
아래의 코드에서 do_something 함수를 호출하면 실행 컨텍스트가 생길 것이다.
function do_something() {
var a = 1;
let b = 2;
while (true) {
var c = 3;
let d = 4;
console.log(b);
break;
}
}
do_something();
ExecutionContext:
LexicalEnvironment:
b -> nothing
outer: VariableEnvironment
VariableEnvironment:
a -> undefined, c -> undefined
outer: global
...
또한 while문에 진입하게 되면 새로운 컨텍스트가 생긴다.
ExecutionContext:
LexicalEnvironment:
d -> nothing
outer:
LexicalEnvironment
b -> 2
outer: VariableEnvironment
VariableEnvironment:
a -> 1, c -> undefined
outer: global
...
while문이 종료되면 원래의 실행 컨텍스트로 복원된다.
ExecutionContext:
LexicalEnvironment
b -> 2
outer: VariableEnvironment
VariableEnvironment:
a -> 1, c -> 3
outer: global
아래의 코드가 에러가 나는 이유는 뭘까?
바로 let
과 const
의 TDZ(Temporal dead zone) 때문이다.
TDZ의 대한 설명은 여기를 클릭하면 볼 수 있다.
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
bar 함수의 매개변수 a에서 b의 참조를 찾을 수 없기 때문에 에러가 난다. TDZ를 이해했다면 당연한 내용일 것이다.
하지만 여기서 정말 중요한 부분은 바로 Lexical Environment를 새로 만든다는 것이다.
무슨 말인지 이해하기 어려울 수도 있기 때문에 코드 하나를 더 보면서 설명하겠다.
const str = 'outerText';
function foo(fn = () => str) {
const str = 'innerText';
console.log(fn());
}
foo(); // 'outerText'
만약 아래의 코드가 새로운 Lexical Environment를 만들지 않았을 때의 상황을 생각해보겠다. (앞의 과정은 생략하고 foo함수 호출부터 설명)
하지만 실제 코드를 실행시켜보면 outerText를 출력한다. 이는 상식적이지 않은 동작이고, 마치 함수 외부에서 내부 스코프를 참조하고 변경시켜버리는 모양이 되어버리고 여러 문제를 일으킬 수 있는 여지가 된다. 이 때문에 기본값 매개변수는 함수의 내부를 참조할 수 없도록 만들어야 한다. 그러기 위해서는 매개변수, 함수 내부 변수들을 Environment부터 분리해서 추가적인 스코프 체인을 만들어야 한다.
따라서 함수가 실행되고 변수들을 초기화할 때는 다음과 같은 동작을 한다.
함수가 호출될 때, 매개변수들을 먼저 초기화하고 기본값 매개변수가 있다면 새로운 Environment를 추가로 만들어서 여기에 함수 내부의 변수들을 등록한다. 이렇게 Environment를 새로 만들어버리기 때문에 기본값 매개변수는 함수 내부를 참조할 수 없으면서 함수 내부에서는 매개변수를 참조할 수 있는 중첩 구조를 만들어낼 수 있다.
실행 컨텍스트는 다음과 같은 경우에 생성된다.
실행 컨텍스트는 두 가지 단계로 생성된다.
Creation Phase에서 하는 일
Execution Phase에서 하는 일