“JS는 인터프리터를 쓰기 때문에 한 줄 한 줄 바로 실행한대.”
사실 JS는 '처음 보는 코드'는 실행 직전 컴파일한다.
그리고 인터프리터로 실행하며, 캐싱한 코드는 최적화 컴파일러가 실행한다.
이 부분을 더 알고 싶으면 다른 포스팅을 참고하자.
console.log(num);
const num = 5;
console.log
를 실행할 때 자바스크립트 엔진은 변수 num
에 대해 아무것도 모를까?
prints();
function prints() {
console.log("이거 출력될까요?");
}
prints();
const prints = () => {
console.log("이건 어떨까요?");
}
function
은 실행되고 화살표 함수
는 에러가 난다. 왜?
우리는 일할 때 계획을 먼저 짜고, 작업을 하고, 중간중간 문서화도 하고, 작업을 마무리하면 퇴근한다.
실행 문맥은 비슷한 역할을 한다.
코드를 실행하기 전에 코드를 어떻게 쓸지 개요를 만들고, 실행하고, 마무리하면 사라진다. (원래는 변수 객체, 활성 객체, 스코프 체인, this 정보가 담긴다는 설명이 유명하다. ECMAScript 버전을 갱신하면서 지금은 표현이 조금 달라졌다.)
ECMAScript
의 실행 문맥 소개.
An execution context is a specification device that is used to track the runtime evaluation of code by an ECMAScript implementation.
At any point in time, there is at most one execution context per agent that is actually executing code. This is known as the agent's running execution context.
The execution context stack is used to track execution contexts. The running execution context is always the top element of this stack.
실행 문맥은 평가한 코드를 따라갈 때 쓰는 특수한 장치입니다.
어느 시점에든, 코드를 실행하는 대행자(agent)는 실행 문맥을 가집니다.
작동중인 실행 문맥(running execution context)이라고도 부릅니다.
콜 스택은 실행 문맥을 따라가고, 작동중인 실행 문맥은 콜 스택 맨 위에 있습니다.
간단히 말하면 실행 문맥 콜 스택에서 GEC
를 실행하다가 함수 코드를 만나면 FEC
를 콜 스택에 올려서 실행하는 것을 말한다.
An agent comprises a set of ECMAScript execution contexts, an execution context stack, a running execution context, an Agent Record, and an executing thread. Except for the executing thread, the constituents of an agent belong exclusively to that agent.
대행자(agent)는 실행 문맥 + 콜 스택 + 대행자 기록(필드 집합) + 실행 스레드입니다. 실행 스레드만 제외하고 나머지 요소들은 대행자가 독점합니다.
첨언 1
agent
와 proxy
는 둘 다 대리인이라고 번역하는데, 차이가 있다.
agent
는 직접 행위를 하는 대행자, proxy
는 둘 사이를 중개하는 중개인 개념으로 바라보면 된다.
첨언 2
예컨대 navigator
객체의 userAgent
는 사용자를 대신해서 행사하는 프로그램이다. 브라우저의 userAgent
는 브라우저다. 관련 오류로는 구글 로그인 실패 요인 중 하나인 403 disallowed_useragent
가 있다.
프록시 서버는 캐싱해서 클라이언트와 서버의 빠른 통신을 돕는데, 그야말로 중개다.
세 종류가 있지만 두 종류만 보는 편이다.
Global execution Context (GEC)
코드 전체 문맥. JS
를 실행할 때 생성.
Functional execution context (FEC)
함수 문맥. 함수
를 실행할 때 생성.
Eval execution context
eval 함수를 실행할 때 생성.
eval 함수는 파싱을 지연하는 용도로서 쓰곤 했는데 어려운 디버깅, 컴파일, 캐시 미지원 등의 이유로 이젠 사용이 권장되지 않으므로 생략한다. eval is evil
이란 관용어까지 있다.
실행 문맥은 다음과 같이 두 단계를 거쳐서 만들어진다.
생성 단계(Creation phase)
변수의 '선언'을 읽는다.
실행 단계(Execution phase)
변수의 '값'을 읽는다.
생성 단계에서는 여러 가지 상태 컴포넌트(state component)가 만들어진다.
코드 평가, 함수, 렐름, ScriptOrModule
등이 있지만 일반적인 개념이 아니므로 넘기겠다.
유명한 개념만 알면 된다.
어휘적 환경은 환경 기록을 가진다.
기록(record)이란 컴퓨터 용어로 필드(field)의 집합을 말한다.
주의 : 환경 기록
, OuterEnv
만 알아도 충분하다. 가볍게 보길 추천한다.
Environment Record is a specification type used to define the association of Identifiers to specific variables and functions, based upon the lexical nesting structure of ECMAScript code. Usually an Environment Record is associated with some specific syntactic structure of ECMAScript code such as a FunctionDeclaration, a BlockStatement, or a Catch clause of a TryStatement. Each time such code is evaluated, a new Environment Record is created to record the identifier bindings that are created by that code.
Every Environment Record has an [[OuterEnv]] field, which is either null or a reference to an outer Environment Record. This is used to model the logical nesting of Environment Record values. The outer reference of an (inner) Environment Record is a reference to the Environment Record that logically surrounds the inner Environment Record. An outer Environment Record may, of course, have its own outer Environment Record.
환경 기록이란 어휘가 중첩되는 구조(lexical nesting structure)임을 이용해 변수, 함수들의 식별자 연결을 정의합니다.
환경 기록은 특정한 구문 구조(함수 선언문, 블록 구문, Try의 Catch 절 등)와도 연관이 깊습니다. 이런 코드를 평가할 때 새 환경 기록을 만들며 해당 코드의 식별자 바인딩을 기록합니다.
모든 환경 기록에는 null, 혹은 외부 환경 기록을 참조하는 [[OuterEnv]] 필드가 있습니다. 환경 기록의 중첩을 모델링하고, 외부 환경들을 가리킵니다.
[[outerEnv]]
는 흔히 배우는 Scope chain
개념이다.
outerEnv는 해당 스코프 외부의 환경 기록들을 가리켜서 외부 식별자를 참조하게 돕는다.
환경 기록은 아래와 같이 세분화된다.
선언적 환경 기록과 ReferenceError
선언적 환경 기록에는 GetBindingValue
라는 추상 메소드가 있다. (JavaScript
가 내부적으로만 사용하는 메소드를 뜻함. 개발자는 사용 못한다.) 이 메소드는 환경 기록에 들어가는 식별자가 바인딩할 게 없거나, uninitialized
를 바인딩하면 ReferenceError
를 throw
한다.
function tester() {
let testing = "outer";
function innerFunc() {
// 에러나는 코드
let testing = testing;
return testing;
}
return innerFunc;
}
const inner = tester();
innerFunc
는 외부의 testing
을 참조하지 않는다.
만약 JS
엔진이 '요령껏' 외부 식별자를 참조하면 에러가 나기 쉬울 것이다. 컴퓨터는 언어의 불확실한(ambiguous) 성질을 싫어한다. 이를 해결하기 위해 많은 규칙을 가지지만, 완벽하진 않다.
innerFunc
의 testing
은 선언문에서 자기 스스로를 참조한다. 자기 참조는 안티 패턴으로서 지양할 부분이다. 아무튼 초기화가 안 일어나기 때문에 ReferenceError
가 난다.
function tester() {
let testing = "outer";
function innerFunc() {
return testing;
}
return innerFunc;
}
const inner = tester();
이 코드는 정상 작동하는데, innerFunc
의 환경 기록에는 testing
식별자가 없기 때문에 외부 환경 기록에 있는 testing
을 참조하기 때문이다.
이렇게 외부 스코프로 타고 나가는 걸 scope chaining
이라 부르고, OuterEnv
의 리스트를 scope chain
이라 부른다. scope chain
은 개발자 도구에서 [[Scopes]]
필드로 확인할 수 있다. 이따 클로저 예시들에서 확인해보자.
W3C
에서는 this
를 다음과 같이 간결히 설명한다.
In JavaScript, the this keyword refers to an object.
Which object depends on how this is being invoked (used or called).
In an object method, this refers to the object.
Alone, this refers to the global object.
In a function, this refers to the global object.
In a function, in strict mode, this is undefined.
In an event, this refers to the element that received the event.
Methods like call(), apply(), and bind() can refer this to any object.
자바스크립트에서 this 지정어는 객체를 참조합니다.
누가 this를 호출하느냐에 따라 객체가 다르게 지정됩니다.
객체 내부 메소드에서 this는 그 객체를 참조합니다.
그냥 this를 쓰면 Global 객체를 참조합니다.
함수 안에서 this는 Global 객체를 참조합니다.
strict 모드에서 함수의 this는 undefined입니다.
이벤트에서 this는 이벤트를 받는 요소를 참조합니다.
call(), apply(), bind() 메소드는 this가 다른 객체를 참조하게 해줍니다.
apply | call | bind | |
---|---|---|---|
인수 | 배열 | 쉼표로 구분 | 객체 |
예시 | apply ( [a,b,c] ) | call ( a,b,c ) | bind ( obj ) |
특이사항 | this를 이 객체로 고정하겠다 |
간단한 예시 몇 가지를 들어보겠다.
const obj = {
str: "this is obj",
innerFunc() {
console.log(this);
},
};
const outerFunc = obj.innerFunc;
const nextObj = {
str: "this is nextObj",
finalFunc: outerFunc,
};
function test() {
outerFunc();
}
function test2(callback) {
callback();
}
obj.innerFunc(); // this === obj
outerFunc(); // this === Global
test(); // this === Global
test2(obj.innerFunc); // this === Global
test2(obj.innerFunc.bind(obj)); // this === obj
test2(obj.innerFunc.bind(nextObj)); // this === nextObj
nextObj.finalFunc(); // this === nextObj
obj.innerFunc()
는 obj
가 호출했기 때문에 this
=== obj
outerFunc()
는 obj
의 메소드를 가져오긴 했지만, 전역에서 호출하므로 this
=== Global
test()
내부에서 실행하는 outerFunc()
는 호출하는 주체가 test
함수이다. 함수에서 this
는 Global
.test2()
내부에서 실행하는 obj.innerFunc
도 어쨌든 test2()
함수에서 호출하므로 this
는 Global
bind
메소드는 this
를 고정시키는 메소드다. test2()
에 this
를 obj
, nextObj
로 바인딩해서 넘겨주면 바인딩된 객체 obj
, nextObj
를 출력한다.nextObj
의 finalFunc
는 innerFunc
를 복사한 것이지만 호출하는 주체는 nextObj
이므로 this
=== nextObj
.Global
로 지정되는 경우는 strict mode
를 쓰면 undefined
로 바뀐다.
별 쓸모없는 잡담이지만 strict mode
를 안 쓰는 기본 모드는 sloppy mode
(조잡한 모드)란 이름이다. (참고로 Class
, module
을 사용하면 자동으로 strict mode
가 적용된다.)
이렇듯 this
는 bind
만 쓰지 않으면 호출을 누가 하느냐에 따라 값이 동적으로 결정된다.
화살표 함수는 좀 다른데, ECMAScript
에서는 this
관련으로 아래와 같이 설명한다.
(결론만 봐도 된다.)
A function Environment Record is a declarative Environment Record that is used to represent the top-level scope of a function and, if the function is not an ArrowFunction, provides a this binding. If a function is not an ArrowFunction function and references super, its function Environment Record also contains the state that is used to perform super method invocations from within the function.
함수 환경 기록은 선언 환경 기록의 하나로서 함수의 최상단(top-level) 스코프를 나타내는 용도로 쓰는 편입니다. 화살표 함수가 아니면 this 바인딩을 합니다.
화살표 함수가 아니고 super를 참조한다면 그 함수의 함수 환경 기록에는 super 메소드 호출을 수행하는데 쓸 상태들도 포함합니다.
[[thisValue]]
, [[ThisBindingStatus]]
필드 등이 있다.BindThisValue( )
메소드 등이 있다. [[ThisBindingStatus]]
=== lexical
이다. ThisBindingStatus
=== lexical
이면 false
를 return
하고 this
를 따로 바인딩하지 않는다. 바인딩할 필요가 없다. 왜냐하면 아래 MDN
의 설명 때문이다.In arrow functions, this retains the value of the enclosing lexical context's this
화살표 함수의 this는 자신을 둘러싼 어휘 문맥의 this를 유지합니다.
결론
- 리터럴 함수의 this는 함수를 호출하는 객체를 가리킨다.
- 화살표 함수의 this는 자신이 정의된 스코프의 this를 가리킨다.
변수 환경은 이름과 다르게 var
전용 공간이다. 이는 let
, const
, var
의 변수 할당 과정이 다르기 때문이다.
In ECMAScript 2015, let and const are hoisted but not initialized
JavaScript Hoisting refers to the process whereby the interpreter appears to move the declaration of functions, variables or classes to the top of their scope, prior to execution of the code.
let과 const는 호이스팅됩니다. 초기화를 안 할 뿐.
호이스팅은 인터프리터가 코드를 실행하기 전에 함수, 변수, 클래스 등의 선언을 스코프에서 참조하는 작업입니다.(마치 그것들을 해당 스코프 맨 위에 끌어올린 것처럼).
JS
에서 변수는 Declaration(선언) -> Initialization(초기화) -> Assignment(할당) 3단계를 거친다.
실행 문맥을 만들 때 var
는 선언과 초기화를 동시에 해서 undefined
이 바인딩된다.
let
, const
는 선언까지만 해서 uninitialized
가 바인딩된다.
let
, const
는 어떤 차이가 있을까?let | const |
---|---|
호이스팅 시 선언만 된다. | 호이스팅 시 선언만 된다. |
초기화 따로, 할당 따로 가능 | 초기화, 할당 동시에 해야 함 |
let num;
num = 2;
const str = "string";
리터럴 함수는 선언, 초기화, 할당 3단계가 한꺼번에 된다.
변수 값이 초기화되기 전의 구간을 TDZ(Temporal Dead Zone, 일시적 사각지대)라고 부른다. 호이스팅했을 때 let
과 const
등은 선언 단계이므로 TDZ
에 속하지만 var
는 초기화가 됐으므로 해당 사항이 없다.
어휘적 환경과 변수 환경은 변수 할당이 다르게 구분되지만 그 외에 돌아가는 원리 자체는 똑같다.
클로저를 말할 때 단골로 나오는 유명한 말이 하나 있다.
JS에서 함수는 1급 객체(first-class-object)이다.
1급 객체는 뭘까?
return
값으로 쓸 수 있고,2급 객체도 있을까? 당연히 있다.
return
불가3급 객체도 있다.
return
불가자바스크립트에서 함수는 1급 객체 조건을 다 만족한다. 그래서 클로저는 함수가 그 점을 이용한다. 함수를 만들 때 함수에서 사용하는 변수가 함수 외부에 있다면, 그 변수는 함수의 환경 기록에서 참조된다.
클로저의 가장 대표격인 예시는 함수가 내부 함수를 return
하는 경우인데 MDN
에서 소개하는 클로저는 다음과 같다.
A closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment).
a closure gives you access to an outer function's scope from an inner function. In JavaScript, closures are created every time a function is created, at function creation time.
클로저는 함수와 함수가 참조하는 어휘적 환경을 묶은 것입니다.
클로저를 쓰면 내부 함수는 외부 함수 스코프에 접근 가능합니다. 클로저는 함수를 만들 때마다 생성됩니다.
위의 말을 보면 알겠지만 일반적인 함수는 전부 클로저를 만든다. new Function
을 제외하고.
new Function
: 외부 렉시컬 스코프 참조 불가능. 전역 렉시컬 스코프 참조.new Function
은 eval
과도 연관이 있는데 현재 포스팅 주제와 사뭇 다르니 넘기겠다.
function tester() {
let testing = "outer";
function innerFunc() {
console.log(testing);
let testing = "inner";
return testing;
}
return innerFunc;
}
const inner = tester();
inner();
inner()
를 실행하면 console
에는 뭐가 찍힐까? 직관적으론 outer
가 출력될 것 같다. 하지만 에러가 난다.
아래부터 나오는 예제 코드들은 예전에 작성했던 예시로 변수 객체, 활성 객체, scope chain, this로 이루어지는 실행 문맥을 의사 코드(pseudocode)로 작성한 것이다.
활성 객체(AO)는 FEC의 식별자 정보, 변수 객체(VO)는 GEC의 식별자 정보를 참조한다.
innerFunc FEC
는 아래와 같이 생성됐을 것이다.
innerFuncExecutionObj = {
scopeChain: [tester, Global],
activationObject: {
testing: <uninitialized>,
}
this: undefined
}
호이스팅은 변수들을 해당 스코프의 맨 위에 올려두고 참조하는 개념이다. innerFunc
에서 testing
은 tester
에서 선언한 testing
이 아니라 안쪽에서 선언한 testing
을 참조하고 있다. 그래서 에러가 난다.
function tester() {
let testing = "outer";
function innerFunc() {
console.log(testing);
return testing;
}
return innerFunc;
}
const inner = tester();
inner();
innerFunc
의 let testing
구문을 지우면 outer
가 출력된다.
원래 FEC
는 함수가 할 일을 다하고 나면 메모리를 회수하며 소멸한다. 다시 말해 inner
에 값을 할당하면 tester()
의 FEC
와 변수들은 소멸해야 한다. 하지만 inner()
를 실행하면 tester
의 변수값을 확인할 수 있다. 클로저 때문이다.
클로저는 함수가 생성되는 순간의 함수 자신과 함수를 둘러싼 유효 환경의 합집합이다. inner
는 클로저에서 tester
의 어휘 환경을 참조하고, tester
스코프 내의 원소들을 사용할 수 있다.
이 때 tester
스코프 내의 원소들을 복사해서 사용하는 게 아니라 원본 그대로 쓰기 때문에 변경도 자유롭게 가능하다.
화면으로 이해해보자.
해당 코드를 라이브 서버로 실행하고 디버깅하는 모습.
Global
은 전역 객체(Global Object)이고 Script
는 HTML
파일이 Script
태그를 읽으면서 만든 것이다.
잠시만 전역 환경 기록과 전역 식별자를 짚고 넘어가자.
하지만 전역 환경 기록 내부가 여러 환경 기록으로 또 세부화된단 뜻은 아니다.
A global Environment Record is logically a single record but it is specified as a composite encapsulating an object Environment Record and a declarative Environment Record.
전역 환경 기록은 이론적으론 단일 기록이지만, 캡슐화 처리한 객체 환경 기록과 선언 환경 기록의 조합으로 명시합니다.
내부적으로 별도 처리를 한다고만 인식하자. 너무 깊게 파면 안 된다.
sub.js
에선 main.js
의 test
함수를 별도로 import
하지 않아도 쓸 수 있다.
그러므로 전역 공간을 사용하는 건 조심할 필요가 있다.
let myVar = "안녕하세요";
globalThis.myVar = "자바스크립트입니다.";
console.log(myVar);
console.log(globalThis.myVar);
// 이 코드를 실행하면 재밌는 출력이 나올 것이다.
이제 다시 클로저로 넘어와보자.
innerFunc
클로저가 tester
에 있던 변수 testing : 'outer'
을 참조하는 모습을 확인할 수 있다.
이처럼 함수 바깥에 있지만 사용 가능한 변수를 자유 변수(free variable)라고 한다.
자유 변수 반대는 묶인 변수(bound variable)라고 부른다.
예 : 모든 전역 변수 => 자유 변수 (필요 조건)
처음에 클로저가 1급 객체가 상관이 있는 것으로 설명했는데, 만약 함수가 2~3급 객체였다면 위와 같이 함수 안에서 함수를 return
하는 코드를 짤 수 없기 때문이다.
예시를 하나만 더 보자.
function counts() {
let count = 0;
function addCount() {
count++;
return count;
}
return addCount;
}
let closure1 = counts();
// count : 1
let count1 = closure1();
let closure2 = closure1;
// count : 2
let count2 = closure2();
console.log(count1, count2);
// 1, 2
closure2
는 closure1
을 참조해서 만들기 때문에 count
가 1인 클로저를 가져온다.
더 자세히 이해하고 싶으면 아래 사이트에 접속해서 과제를 풀어보자.
https://ko.javascript.info/closure
클로저와 메모리는 상관이 깊다. 함수를 다 실행한 뒤에도 그 함수 스코프의 변수들을 사용할 수 있다는 것은, 메모리 회수를 안 했기 때문이다.
클로저를 만들었다고 해서 모든 코드가 클로저를 계속 사용한단 법은 없는데, 안 쓰는 클로저가 쌓이면 클로저를 보관하는 메모리도 누적된단 얘기다.
따라서 클로저를 만드는 코드를 null
처리하는 방법 등으로 가비지 컬렉터가 메모리를 회수하도록 간접 처리를 해야 한다. C++
같은 특수를 제외하면 JS
를 포함한 어지간한 언어는 가비지 컬렉션 직접 조작이 불가능하다. (직접 조작이 가능하면 마냥 좋은 게 아니다. 문제가 나기 쉽다는 뜻이기도 하다.)
자동으로 메모리를 관리하는 언어들은 보통 Mark & Sweep
알고리즘으로 가지가 안 닿는(사용을 안 하는) 메모리를 회수한다. GC
의 원리가 그렇기 때문에 안 쓰는 클로저 함수를 null
처리하면 GC
가 해당 메모리를 처리하는 것. 다만 가벼운 프로젝트에서 이 부분을 예민하게 신경써야할 만큼 우리의 컴퓨터 메모리가 빡빡하진 않으므로 초보라면 다른 기초를 더 신경쓰길 권한다.
전역 변수는 메모리 회수 대상이 아니다. 이로 인해서 발생할 수 있는 간단한 이슈로는 DOM
조작이 있겠다. 삭제할 노드를 전역 변수로 선언하고 삭제 처리하면 화면에선 그 노드가 사라져도 메모리에는 남기 때문. 콜백 함수 내부에서 삭제할 노드를 불러내고 처리하는 게 낫다.
백문이 불여일견, 매우 간단한 예시를 하나 보자.
button1
노드를 전역 변수로 선언한다.lexicalEnvironment()
함수를 쓰면 button1
은 제거된다.lexicalEnvironment()
함수는 printf
함수를 return
하면서 클로저를 생성한다. 클로저에는 자유 변수 freeVariable
이 포함될 것이다.lexicalEnvironment()
함수의 결과물을 closure
라는 변수에 담아낸다.closure
에 콜백함수를 전달해본다. 클로저가 정상 작동하면 freeVariable
이 잘 참조될 것이다.btn1
과 클로저는 사용이 끝났지만 메모리에 계속 남을 것이다.closure
변수에 내용이 담기기 전 단계.closure
변수에 할당될 것이다.btn1
은 이미 삭제 처리가 됐음에도 Script
에 남아있는 모습을 볼 수 있다.모든 작업이 다 끝났지만 사용할 일이 없는 btn1
과 클로저가 메모리에 남아있는 모습을 볼 수 있다. 이를 해결하려면 어떻게 할까?
function lexicalEnvironment() {
// 삭제할 노드를 함수 내부에서 선언
const btn1 = document.getElementById("button1");
const freeVariable = "자유 변수";
btn1.remove();
function printf(callback) {
const str = freeVariable + "입니다.";
callback(str);
}
return printf;
}
let closure = lexicalEnvironment();
closure((str) => console.log(str));
// 클로저를 해제.
closure = null;
const num = 1;
btn1
과 클로저가 말끔하게 비워진 모습을 볼 수 있다.
원래 내부 함수는 외부 변수를 자유 변수로서 쓰기 때문에, 내부 함수는 열린 함수이다. (자유 변수를 쓰는 함수를 열린 함수라 한다.)
하지만 클로저는 메모리 회수 대상이 아니다. 클로저에 있는 변수는 보이지는 않지만 계속 쓸 수 있다. 클로저로 인해 내부 함수는 외부 환경과 한 덩어리로 묶여서 닫힌 함수가 된다. 열려있던 것을 닫는 행위. 그래서 closure
이다.
"굳이 전역 변수를 남발하지 말고, 코드 정리 잘하자." 정도로만 받아들이자. 기본에 충실한 게 좋다. 사실 나도 클로저 비워서 메모리 관리해야겠단 생각도, 시도도 한 적 없다. 위에도 적었듯이 메모리 관리는 문제가 터지기 쉽다.
확실한 근거 없이 가비지 컬렉션을 다루면 나중에 문제 터졌을 때 원인도 못 찾고 X될 확률이 높다. 우리의 메모리는 그렇게까지 나약하지 않으니 너무 기우 가지지 말자.
유명한 말 세 가지를 소개하며 TMI는 여기까지만 하겠다.
효율이라는 명분 하에 저질러지는 죄악이 많다. (제대로 하지도 못하면서)
-William A. Wulf
웬만하면 머리에서 효율이란 말을 지워라. 섣부른 최적화가 만악의 근원이다.
-Donald E. Knuth
“최적화는 아래의 두 규칙만 따르면 된다.”
1. 하지마.
2. (네가 전문가라면) 아직 하지마. 완벽, 명쾌하지 않으면 하지마라.
-M. A. Jackson
각 변수에 값을 할당한다. GEC의 실행 단계는 다음과 같이 이뤄진다.
실행 문맥은 스택 형태로 쌓여서 맨 위에 있는 문맥부터 실행한다.
globalExecutionObj = {
scopeChain: [],
variableObject: {
ten: 10,
two: 2,
one: 1,
},
this: window
}
GEC
, FEC
를 같이 예시로 보자.
let firstName = 'Zelda';
function nameMaker(name){
let lastName = 'Link';
let fullName = firstName + lastName;
}
nameMaker(firstName);
// 생성 단계 GEC
globalExecutionObj = {
scopeChain: [],
variableObject: {
firstName: <uninitialized>,
nameMaker: func,
},
this: window
}
// 실행 단계 GEC
globalExecutionObj = {
scopeChain: [],
variableObject: {
firstName: 'Zelda',
nameMaker: pointer to function nameMaker,
},
this: window
}
GEC
에서 함수는 생성 단계에서 함수라고 인식하고, 실행 단계에서 해당 함수의 포인터를 부여한다.
// 생성 단계 FEC
nameMakerExecutionObj = {
scopeChain: Global,
activationObject: {
arguments: {
0: name,
length: 1
},
name: 'Zelda',
lastName: <uninitialized>,
fullName: <uninitialized>,
},
// 'use strict' 모드로 하면 this가 undefined로, 안 쓰면 Global로 나온다.
this: Global or undefined
}
// 실행 단계 FEC
nameMakerExecutionObj = {
scopeChain: Global,
activationObject: {
arguments: {
0: name,
length: 1
},
name: 'Zelda',
lastName: 'Link',
fullName: 'ZeldaLink',
},
this: Global or undefined
}