제가 자바스크립트에서 가장 중요하게 생각하는 것들(실행 컨텍스트, 이벤트 루프, 프로토타입) 중 하나인 실행 컨텍스트 2편입니다. 실행 컨텍스트와 이벤트 루프 분석만 잘 할 줄 알아도 코드를 실행하지 않고도 모든 코드의 실행 순서와 결괏값을 파악할 수 있다고 했죠. 저는 항상 저만의 방식으로 정리하고 있었는데 간만에 또 공유해봅니다.
*주의* 공식 스펙에 적힌 내용과는 좀 다릅니다. 하지만 이렇게 분석해도 99% 정확한 결과가 나옵니다. 나머지 1% 경우를 발견하면 제보해주세요.
안녕하세요. 예전 실행 컨텍스트 글에서 변수 스코프, this, arguments 등을 한 번에 설명하는 것을 보여드렸는데요. 블록 스코프인 let과 const까지 꼈을 때는 어떻게 이해하면 되냐는 질문이 많았습니다. 예전 글은 let과 const에 관한 내용이 없고 var에만 해당했습니다. 그래도 이 글을 읽기 전에 예전 글을 먼저 읽고 오셔야 수월하게 이 글을 읽을 수 있을실 겁니다.
블록 스코프
let과 const가 추가되었다고 많이 달라진 것은 아니고요. 이전에 이해하신 것에서 블록 스코프를 하나 더 추가하면 됩니다. 다음 코드를 한 번 볼까요.
let name = 'zero';
let age = 28;
const wow = (word) => {
let name = 'nero';
if (true) { // (2)
let name = 'hello';
console.log(age);
}
console.log(word + ' ' + name); // (3)
}
wow('hi'); // (1)
실행 컨텍스트를 객체 형식으로 표현하자면 다음과 같습니다.
'전역 컨텍스트': {
변수객체: {
arguments: null,
variable: ['name', 'age', 'wow'],
},
scopeChain: ['전역 변수객체'],
this: window,
}
전역 변수 객체는 var때와 같습니다.
이제 wow 함수가 호출되는 상황을 보죠. 주석 (1)번 상황입니다. 함수를 호출했으니 새로운 컨텍스트가 생기는 것도 같습니다.
'전역 컨텍스트': {
변수객체: {
arguments: null,
variable: ['name': 'zero', 'age': 28, 'wow': 함수],
},
scopeChain: ['전역 변수객체'],
this: window,
}
'wow 컨텍스트': {
변수객체: {
arguments: ['hi'],
variable: ['name': 'nero'],
},
scopeChain: ['전역 변수객체', 'wow 변수객체'],
this: window,
}
블록 스코프에서는 분석이 조금 달라집니다. 주석 (2) 부분이 실행될 때의 상황입니다. if 문은 블록 컨텍스트를 가지므로 wow 컨텍스트 아래에 블록 컨텍스트가 하나 더 생깁니다. 블록은 { } 라고 생각하시면 됩니다. while, if , for, function 등에서 볼 수 있는 { }가 맞습니다. 객체 리터럴의 { }는 아니고요.
'wow 컨텍스트': {
변수객체: {
arguments: ['hi'],
variable: ['name': 'nero'],
},
scopeChain: ['전역 변수객체', 'wow 변수객체'],
this: window,
}
'if 블록 컨텍스트': {
변수객체: {
variable: ['name': 'hello'],
},
scopeChain: ['전역 변수객체', 'wow 변수객체', 'if 블록 변수객체'],
}
블록 컨텍스트에서는 variable과 scopeChain만 있다고 보면 됩니다. arguments는 함수가 아니라서 없고, this는 상위 this를 따라가므로 필요 없습니다.
if 문이 끝나는 순간 블록 컨텍스트는 사라집니다. 따라서 if 문 안에서 name에 접근하면 hello가 되고, if문 바깥에서 name에 접근하면 nero가 됩니다. if 문 안에서 age에 접근하면 scopeChain을 따라 거슬러올라가 전역 변수객체에서 age를 찾아냅니다.
정리하자면 let, const의 도입때문에 실행 컨텍스트 분석이 달라졌다기보다는 블록 스코프의 도입 때문에 달라졌다고 보시면 됩니다. 블록 스코프가 생길 때마다 컨텍스트를 하나 더 만들어두시면 됩니다. 컨텍스트는 블록이 끝나면 사라집니다. 함수 컨텍스트도 { }를 가지고 있으므로 블록 컨텍스트의 일종입니다.
참고로 그냥 { }를 써도 블록이 됩니다. 따라서 이것도 블록 컨텍스트를 만들어두셔야 합니다.
let hello = '1';
{
let hello = '2';
console.log(hello);
}
console.log(hello);
for문 분석
for문에서의 컨텍스트도 많이 궁금해하시는 것 같습니다.
for (let i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i);
});
}
다음 결과는 0, 1, 2이지만 var을 쓰면 완전히 달라집니다.
for (var i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i);
});
}
3, 3, 3이 나옵니다. 결괏값이 확연히 차이나는 이유는 역시나 스코프 때문입니다.
var의 경우에는 컨텍스트 분석이 다음과 같이 됩니다. 변수 i가 전역컨텍스트에 생성되기 때문이죠. 일단 i가 3이 된 뒤에 setTimeout이 세 번 호출되는 건 알고 계셔야 합니다.
'전역 컨텍스트': {
변수객체: {
arguments: null,
variable: ['i': 3],
},
scopeChain: ['전역 변수객체'],
this: window,
}
'setTimeout 컨텍스트': {
scopeChain: ['전역 변수객체', 'setTimeout 변수객체']
}
'setTimeout 컨텍스트': { // 앞에 setTimeout 컨텍스트가 사라진 후 생성됨
scopeChain: ['전역 변수객체', 'setTimeout 변수객체']
}
'setTimeout 컨텍스트': { // 앞에 setTimeout 컨텍스트가 사라진 후 생성됨
scopeChain: ['전역 변수객체', 'setTimeout 변수객체']
}
setTimeout 컨텍스트는 편의상 scopeChain만 표시했습니다. 따라서 setTimeout 컨텍스트에서 i를 찾으면 i는 3이 될 수밖에 없습니다.
let의 경우는 다음과 같이 생성됩니다.
'전역 컨텍스트': {
변수객체: {
arguments: null,
variable: null,
},
scopeChain: ['전역 변수객체'],
this: window,
}
'for i = 0 컨텍스트': {
variable: [i: 0],
scopeChain: ['전역 변수객체', 'for i = 0 변수객체']
}
'for i = 1 컨텍스트': {
variable: [i: 1],
scopeChain: ['전역 변수객체', 'for i = 1 변수객체']
}
'for i = 2 컨텍스트': {
variable: [i: 2],
scopeChain: ['전역 변수객체', 'for i = 2 변수객체']
}
'setTimeout 컨텍스트': {
scopeChain: ['전역 변수객체', 'for i = 0 변수객체', 'setTimeout 변수객체']
}
'setTimeout 컨텍스트': {
scopeChain: ['전역 변수객체', 'for i = 1 변수객체', 'setTimeout 변수객체']
}
'setTimeout 컨텍스트': {
scopeChain: ['전역 변수객체', 'for i = 2 변수객체', 'setTimeout 변수객체']
}
전역 컨텍스트에는 아무런 변수가 할당되지 않고, for문을 돌 때마다 컨텍스트가 하나씩 생성된다고 보면 됩니다. 그 안에서 다시 setTimeout 컨텍스트가 생성되므로 setTimeout 안에서 i에 접근하면 각각 0, 1, 2가 나옵니다.
궁금한 점은 for문 분석에서,
저는 var도 let과 마찬가지로 for문에 해당하는 블럭 컨텍스트가 생성되고, i값을 스코프 체인을 통해 전역에서 가져올것 같았습니다.
setTimeout컨텍스트가 3개 생성된 것처럼, for 컨텍스트가 3번 생성되고(프로퍼티가 전부 동일한), i값은 전역 변수객체에서 3으로 가져오는 그림일 거라고 생각했었거든요.
간략하게 표현하신 것으로 이해하면 될까요? 아니면 원래 저렇게 생성되는게 맞는걸까요?
컨텍스트는 변수 때문에 생성되는게 아닌 전역에서 한번 생성 + 함수마다 생성 + ES2015부턴 블럭이 있을때마다 생성되는거로 이해하고 있었거든요!
예를들어, var를 사용했을 때라도 for 컨텍스트는 아래처럼 생성되는게 아닌가 하고 생각했습니다.
for 컨텍스트: {
variable: [ 없음 ]
scopeChain: ['전역 변수객체', 'for 변수객체']
this: 상위 this
}
i값을 찾으려고 할때 for에 없으니까 스코프 체인을 통해 i를 가져오구요!
이게 정말로 생성되는 것인데 간략하게 표현하신건지(제로초님만의 정리 방식? 으로),
아니면 원래 블럭이 있더라도 변수가 없으면 원래 컨텍스트는 생성되지 않는건지가 궁금합니다! 😂