Lexical Environment 와 Closure에 대한 이해

Paul Kim·2020년 8월 25일
13

Deep Dive

목록 보기
1/1

본 글은 다음의 블로그 글로 저랑 해당 내용에 대해서 이야기를 하셨던 분이 조금 더 잘 이해할 수 있도록 몇 가지 내용을 생략하거나 의미를 간소화해서 번역한 내용입니다. 보다 정확한 출처표기는 글의 맨 아래에 해놓았습니다.

Execution Context

JavaScript 해석기는 우리가 함수 혹은 스크립트를 실행 하려고 할 때 새로운 Context 를 만들어낸다.

여기서 Context 란?

  • 비유: 명사 - 동사
  • xx님 저랑 같이 코드(context) → xx님 저랑 같이 코드해요(함수에 의해 깨어날때)

모든 스크립트나 코드는 전역 실행 콘텍스트 (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 component 'VariableEnvironment: { ... }' 가 변수, arguments, 그리고 함수선언을 최초로 저장하는데에 쓰인다. Var 로 선언 된 변수들은 "Undefined" 인 상태에서 시작된다.
  • This 의 값이 결정된다.
  • LexicalEnvironment 란 그냥 현재단계 에서는 VariableEnvironment 의 "카피본" 으로 인식해라.

다음은 실행 단계에서 벌어지는 일들이다.

  • 값이 할당된다: 기억하세요, 최초로 선언 된 변수들은 "Undefined" 상태에서 시작한다는 것을.
  • LexicalEnvironment 이 사용 되는 이유는 ThisBinding: 에 묶여있는 값을 풀어주기 위함이다.

Lexical Environment

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 가 가능한 것임.

Identifier Resolution (혹은 Scope chain lookup)

클로져를 이해하기 전에 scope chain 이 execution context 상에서 어떻게 만들어지는지 이해해야한다.

우리가 앞서 보았듯이, 각 실행 context 들은 Identifer Resolution 에 쓰여지는 Lexical Environment 를 갖고 있음. 모든 local 에서 묶여있는 값들은 environment record table 에 저장 된다. 만약 식별자들이 현재의 environmentRecord 에서 풀리지 않는다면, 이 풀리는 과정이 바깥(부모)의 environment record table 까지 넘어감. 이러한 패턴은 식별자가 비로서 확인될 때 까지 계속 진행될꺼고, 만약 끝까지 식별자에 대한 값을 못찾으면 Reference Error 가 발생한다.

프로토타입 LookUp Chain 도 이와 매우 유사하다. 여기서 꼭 기억해야 하는 것은 LexicalEnvironment 는 생성단계 에서 바깥의 묶여있는 값을 고정적으로 가져간다는 것이며 실행 단계 동안 사용이 되는 것..

Closures

함수 생성 단계에서 봤듯이, 내부 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

결론

  • Execution context stack 은 LIFO(후입선출) 자료 구조를 따라간다.
  • 우리의 코드가 실행 될 때 하나의 Global Context 만 존재.
  • 함수를 호출한다 = 새로운 execution context 를 생성한다. 만약 내부 함수의 호출이 있다면, 새로운 context 가 만들어지며 이 새로운 context 는 부모 context 의 위로 올라감. 함수가 실행을 완료할 때 stack 에서 pop 되며, 실행 단계는 stack 밑으로 돌아감.
  • Lexical Environment 은 두개의 구조를 갖고있음 components: environmentRecord and reference to outer environment.
  • VariableEnvironment and LexicalEnvironment 는 둘다 고정적으로 context 에 만들어진 내부 함수에 의해 가져가짐.
  • 생성 단계에서 모든 함수는 바깥의 부모 environment 묶여이는 값을 가져온다. 이것이 내부 함수들로 하여금 부모 context 들이 execution stack 에서 지워지더라도 바깥에 있는 값들을 access(진입) 할 수 있게 만드는 가장 큰 이유다. 이 매커니즘이 자바스크립트에서 클로져의 기본 작동원리다.

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

1개의 댓글

comment-user-thumbnail
2022년 3월 20일

안녕하세요 포스트 정말 잘 보았습니다. 많은 도움 되었어요. 감사합니다.
(결론에는 후입선출이라고 잘 적혀있는데 첫 번째 그림 아래에 선입선출이라고 잘못 적혀 있어요~)

답글 달기