본 글은 다음의 블로그 글로 저랑 해당 내용에 대해서 이야기를 하셨던 분이 조금 더 잘 이해할 수 있도록 몇 가지 내용을 생략하거나 의미를 간소화해서 번역한 내용입니다. 보다 정확한 출처표기는 글의 맨 아래에 해놓았습니다.
JavaScript 해석기는 우리가 함수 혹은 스크립트를 실행 하려고 할 때 새로운 Context 를 만들어낸다.
여기서 Context 란?
모든 스크립트나 코드는 전역 실행 콘텍스트 (excution context) 와 시작한다.
우리가 함수를 호출 할때 새로운 execution context 가 생성되어, execution stack의 맨 위로 올려진다.
마찬가지로 다른 함수에 포함 되어있는 함수가 전혀 다른 함수에 포함되어 있는 함수를 실행 시킬 때도 같은 상황이 발생.
선입선출
다음과 같이 함수를 실행할 때 아래와 같은 일이 벌어진다. :
global execution context 가 생성되고 execution stack 의 가장 아래에 위치하게 된다 .
bar 라는 함수를 깨울 때 (호출), 새로운 bar 함수의 execution context 가 만들어지며 global execution context 의 맨 꼭대기에 올라간다.
bar 함수가 자기 안에 있는 foo 함수를 깨울 때 새로운 foo execution context 가 만들어 지며 bar execution context 위에 위치하게 된다.
함수에서 foo 를 리턴 할 때, foo 의 context 가 가장 먼저 스택에서 제거 되며, bar context 로 돌아온다.
bar 실행이 끝나면 다시 global context 로 돌아오며, 비로서 스택이 비워진다.
Execution stack 은 후입선출 자료 구조로 작동한다. Execution stack 은 밑에 있는 context 을 실행하기 이전 가장 위에있는 execution context 가 리턴 되는 것을 기다린다.
개념적으로 Execution Context 는 다음과 같은 구조로 되어있다:
// Execution context in ES5ExecutionContext = {
ThisBinding: <this value>,
VariableEnvironment: { ... },
LexicalEnvironment: { ... }
}
여기서 반드시 이해해야 할 부분은 execution context 를 호출할 때 마다 두가지 단계가 있다는 것이다: Creation Stage (생성 단계) - Execution Stage (실행 단계). Creation Stage(생성 단계는) context 는 만들어 졌으나 깨어 있지 않는 상태를 의미한다. 호출하는 순간 생성 단계는 이미 실행된다. 깨울 때는 실행 단계가 시작된다. 호출과 깨우다를 구분하자.
다음은 생성 단계에서 벌어지는 일들이다:
'VariableEnvironment: { ... }'
가 변수, arguments, 그리고 함수선언을 최초로 저장하는데에 쓰인다. Var 로 선언 된 변수들은 "Undefined" 인 상태에서 시작된다.다음은 실행 단계에서 벌어지는 일들이다.
ECMA Script 스펙 262 에 따르면: Lexical Environment 란 스펙 종류의 일종으로 Identifier (편의상 식별자) 와 특정 변수, 그리고 ECMAScript 코드에서 lexical nesting 구조에 의해 짜여진 함수들의 관계를 정립 해주기 위한 기능을 한다.
간단하게 얘기해서, Lexical Components 는 두 가지 요소(components)를 갖고 있다: environment record, 그리고 **lexical environment 에 대한 reference, 즉 (부모) .**
Identifier Resolution
이 3 가지 요인 때문에 ECMAScript 모든 함수는 Closure 다.
var x = 10;
function foo(){
var y = 20;
console.log(x+y); // 30
}
// Environment 은 기술적으로 두가지 main components 를 갖고 있음:
// **environmentRecord, 와 reference to the outer environment(부모)**
// Global Context 의 Environment
globalEnvironment = {
environmentRecord: {
// 이미 만들어진
// 우리가 갖고 있는 묶여있는 값
x: 10
},
outer: null // 부모 Environment 가 없다 라는 의미
};
// "foo" function의 Environment
fooEnvironment = {
environmentRecord: {
y: 20
},
outer: globalEnvironment
};
Lexical Environment 를 그림으로 보자면 이렇게 되어있다:
보이다시피 식별자인 "x"를 foo context 에서 풀어주려고 할때,바깥 environment (global) 까지 화살표가 뻗어나간 것을 볼 수 있다. 이 과정을 "identifier resolution" 이라고 부르며, 이 과정은 execution context 를 실행할 때 발생한다.
여기까지 배운 Environment 에 대한 구조를 복기 해보고 다시 Execution context 의 구조로 돌아가 보자.
VariableEnvironment: 이 것의 environmentRecord 는 최초의 변수, arguments, 함수 선언들을 저장하기 위해 사용된다. 이후 Context 가 활동하는 단계로 들어갈 때 해당 값들이 채워짐.
function foo(a) {
var b = 20;
} foo(10);// **생성단계에서** foo 함수의 context VariableEnvironment component.
fooContext.VariableEnvironment = {
environmentRecord: {
arguments: { 0: 10, length: 1, callee: foo },
a: 10,
b: undefined
},
outer: globalEnvironment
};
// 실행 단계가 끝나면, VariableEnvironemnt Environment Record 의 테이블이 (위 그림)
값들로 채워짐.
fooContext.VariableEnvironment = {
environmentRecord: {
arguments: { 0: 10, length: 1, callee: foo },
a: 10,
b: 20
},
outer: globalEnvironment
};
LexicalEnvironment: 초기에 lexicalEnvironment 는 그냥 VariableEnvironment 의 카피버전이다. Context 가 실행 되고 있을 때, context 에서 등장하는 식별자들이 어디에 묶여있을 것인지 결정 한다.
주의 VE and LE 둘다 lexical environments 임.
즉, 실행 단계에서 둘다 context 에서 생성 된 내부 함수에서 '고정적으로' 바깥의 묶여있는 값들을 캡쳐한다.
이러한 원리 때문에 closures 가 가능한 것임.
클로져를 이해하기 전에 scope chain 이 execution context 상에서 어떻게 만들어지는지 이해해야한다.
우리가 앞서 보았듯이, 각 실행 context 들은 Identifer Resolution 에 쓰여지는 Lexical Environment 를 갖고 있음. 모든 local 에서 묶여있는 값들은 environment record table 에 저장 된다. 만약 식별자들이 현재의 environmentRecord 에서 풀리지 않는다면, 이 풀리는 과정이 바깥(부모)의 environment record table 까지 넘어감. 이러한 패턴은 식별자가 비로서 확인될 때 까지 계속 진행될꺼고, 만약 끝까지 식별자에 대한 값을 못찾으면 Reference Error 가 발생한다.
프로토타입 LookUp Chain 도 이와 매우 유사하다. 여기서 꼭 기억해야 하는 것은 LexicalEnvironment 는 생성단계 에서 바깥의 묶여있는 값을 고정적으로 가져간다는 것이며 실행 단계 동안 사용이 되는 것..
함수 생성 단계에서 봤듯이, 내부 context 에서 LexicalEnvironment 고정적으로 바깥에 있는 값을 저장하려고 할때마다 Closure 가 발생한다. 이 부분은 function 이 이후에 깨워지건 상관없음. 예시를 보자.
var a = 10;
function foo(){
console.log(a);
};function bar(){
var a = 20;
foo();
};bar(); // will print "10"
foo 의 LexicalEnvironment 는 생성 단계에서 "a" 로 묶여있었 던 (값: 10) 를 가져간다. 그래서 만약 foo 함수가 이후에 깨어 날때 (실행 단계에서) "a" 에 대한 식별자는 20이 아닌 10으로 풀려나는 것이다!
개념적으로, identifier resolution (즉 식별자 관계가 정립되는) 과정은 아래와 같음:
"고정적으로 바깥에 묶여있는 값을 가져간다"
Example 2:
function outer() {
let id = 1;return function inner(){
console.log(id);
}
};const innerFunc = outer(); innerFunc(); // 1을 출력;
바깥의 함수가 리턴 될때, execution stack에서 이 함수의 실행 context 는 pop 된다. 하지만 안에있는 함수인 innerFunc() 을 깨울 때, 올바른 값을 출력 하는 이유는 내부 함수의 LexicalEnvironment가 생성되었 을 때 부모의 묶여진 "id" 값을 성공적으로 가져갔기 때문이다.
// check for binding "id" in the env record of "inner"
-- inner.[[LexicalEnvironment]].[[Record]] --> not found// if not found, check for its outer environment (outer)
--- outer[[LexicalEnvironment]][[Record]] --> found 1// resolve the identifier with a value of 1
Singh, A. (2020, August 14). Lexical Environment - The hidden part to understand Closures. Retrieved August 25, 2020, from https://medium.com/@5066aman/lexical-environment-the-hidden-part-to-understand-closures-71d60efac0e0
안녕하세요 포스트 정말 잘 보았습니다. 많은 도움 되었어요. 감사합니다.
(결론에는 후입선출이라고 잘 적혀있는데 첫 번째 그림 아래에 선입선출이라고 잘못 적혀 있어요~)