자바스크립트 엔진은 다음과 같은 두 가지 주요 구성 요소로 이루어져 있다.
실행 컨텍스트는 실행할 코드에 제공할 환경 정보들을 모아놓은 객체이다.
어느 시점에 콜 스택에 실행 컨텍스트가 쌓이게 될까?
다음의 예제를 살펴보자!
var a = 1;
function outer() {
function inner() {
console.log(a);
var a = 3;
}
inner();
console.log(a);
}
outer();
console.log(a);
① 먼저 위의 자바스크립트 코드를 실행하는 순간 전역 컨텍스트가 생성되고 콜 스택에 담긴다.
② 그 다음에 outer
함수가 호출되는 순간 outer
실행 컨텍스트가 생성되고 콜 스택에 담긴다.
이때, 전역 컨텍스트 위에 outer
실행 컨텍스트가 쌓이는데 콜 스택이 이름 그대로 스택 구조이기 때문이다!
③ 그 다음에 inner
함수가 호출되는 순간 inner
실행 컨텍스트가 생성되고 콜 스택에 담긴다.
④ inner
함수 실행이 종료되며 콜 스택에서 inner
실행 컨텍스트가 제거된다.
⑤ outer
함수 실행이 종료되며 콜 스택에서 outer
실행 컨텍스트가 제거된다.
마지막 줄의 a
를 콘솔에 출력하는 문까지 실행되면 더는 실행할 코드가 남지 않는다.
⑥ 이때 전역 컨텍스트가 제거되고, 콜 스택에는 아무것도 남지 않는다.
스크립트를 처음 마주할 때 전역 컨텍스트를 생성하고,
엔진이 스크립트를 쭉 읽어내려가면서 함수 호출을 발견할 때마다
함수의 실행 컨텍스트를 스택에 push
한다.
이렇게 콜 스택은 이름 그대로 스택이기 때문에 FIFO 구조를 가진다.
콜 스택에 실행 컨텍스트가 차곡차곡 쌓이면서 코드의 환경과 순서를 보장한다.
그렇다면 실행 컨텍스트에는 어떤 정보가 담길까?
VariableEnvironment
, LexicalEnvironment
, ThisBinding
이 담긴다.
각각에 대하여 더 세부적으로 살펴보자!
LexicalEnvironment
에는 environmentRecord
와 outerEnvironmentReference
로 구성되어 있다.
environmentRecord
에는 식별자 정보가 담겨 있다.
식별자 정보는 매개변수의 이름, 함수 선언, 변수명 등이다.
위의 예시 코드에서 식별자를 찾아보자면 a
, outer
, inner
가 될 것이다.
함수 컨텍스트가 생성될 때 함수 내의 코드 전체를 순차적으로 살펴보면서 식별자를 수집한다.
이렇게 환경 레코드에 등록된 식별자들은 코드 실행 중에 접근할 수 있게 된다.
즉, 환경 레코드는 변수와 함수의 선언을 저장하고 관리하는 역할을 한다.
여기서 호이스팅이라는 개념이 등장한다.
호이스팅이란 선언문이 코드의 선두로 끌어 올려진 것처럼 동작하는 자바스크립트 고유의 특징이다.
아래의 예시를 통해 호이스팅을 이해해보자.
console.log(result);
var result = 100;
console.log(result);
위의 코드는 아래의 코드와 동일하게 동작한다.
var result;
console.log(result); // undefined 출력
result = 100;
console.log(result); // 100 출력
자바스크립트에서 변수와 함수는 호이스팅 된다고 흔히 알려져 있지만
실제로는 선언문이 코드의 선두로 끌어올려져 동작하지는 않고,
식별자가 코드 실행 이전에 실행 컨텍스트의 environmentRecord
에 등록된다!
이 과정에서 var 키워드로 선언된 변수는 기본값인 undefined
로 초기화된다.
outerEnvironmentReference
은
현재 호출된 함수가 선언될 당시의 LexicalEnvironment
을 참조한다.
위 문장으론 아직 outerEnvironmentReference
가 무엇인지 잘 와닿지 않는다.
이를 이해하기 전에 우선 스코프의 개념에 대해 알아야 한다.
스코프란 식별자에 대한 유효 범위이다.
var outer = function () {
var a = 1;
var inner = function () {
console.log(a);
};
inner(); // 1 출력
};
outer(); // 1 출력
console.log(a); // Uncaught ReferenceError: a is not defined
위의 예시 코드를 살펴보자!
스코프는 함수에 의해서 생성되며 outer
라는 함수 내에서 선언한 a
라는 변수는
오직 outer
함수 내부에서만 접근할 수 있다!
outer
함수 외부에서는 a
변수에 접근할 수 없는데
이는 식별자에 대한 유효 범위가 존재하기 때문이다!
여기서 주목해야 할 점은 다음과 같다!
inner
함수 내부에는 a
라는 변수가 정의되어 있지 않으며,
outer
함수 내부에 inner
함수와 a
변수가 선언되어 있다.
그러나 inner
함수를 실행하면 a
가 정상적으로 출력된다!
왜일까? 🤔
inner
의 실행 컨텍스트 내부의 environmentRecord
에는 변수 a
가 등록되어 있지 않다.
원래대로면 a
가 없기 때문에 에러가 발생하거나 undefined
가 출력될 것이다.
그러나 실제로는 outer
의 실행 컨텍스트로 거슬러 올라가 a
를 검색하고 1을 출력한다.
이처럼 식별자의 유효 범위를 안에서부터 바깥으로 차례로 검색해나가는 것을
스코프 체인이라고 한다.
이는 inner
의 실행 컨텍스트에서 outerEnvironmentReference
가
outer
의 LexicalEnvironment
를 참조하고 있기 때문에 가능한 일이다!
이처럼 outerEnvironmentReference
는 함수가 선언될 당시의
활성화된 실행 컨텍스트의 LexicalEnvironment
에 접근한다.
자신이 속한 부모 함수의 LexicalEnvironment
를 참조하고 있다고 생각하면 이해가 편하다.
위의 예시 코드를 바탕으로 실행 컨텍스트를 그림으로 표현하면 다음과 같다.
LexicalEnvironment
에 대한 설명을 모두 마쳤다.
다시 처음으로 돌아가서 실행 컨텍스트에는
VariableEnvironment
, LexicalEnvironment
, ThisBinding
이 담긴다고 했다!
VariableEnvironment
에 담기는 내용은 LexicalEnvironment
와 같다.
그러나 VariableEnvironment
은 최초 실행 시의 스냅샷을 쭉 유지하며,
코드 진행에 따라 LexicalEnvironment
의 내용은 달라진다는 차이점이 있다.
실행 컨텍스트의 ThisBinding
에는 this
로 지정된 객체가 저장된다.
어떤 함수를 호출할 때는,
함수로서 호출하는 경우와 메서드로서 호출하는 경우 2가지가 있는데
경우에 따라 this 바인딩을 다르게 한다.
var func = function (x) {
console.log(this, x);
};
func(1);
var obj = {
method: func
};
obj.method(2);
위의 코드에서 func
이라는 동일한 함수를 그냥 함수로서 호출하는 경우와
obj
객체의 프로퍼티로 등록 후 객체의 메서드로서 호출하는 경우를 확인할 수 있다.
func
을 그냥 함수로서 호출하면 this
는 전역 객체인 window
가 된다.
그러나 프로퍼티 접근 연산자(.)로 연결하여 객체의 메서드로서 호출하면 this
는 obj
가 된다.
실제로 자바스크립트 엔진은 점(.) 연산자의 유무로 메서드로서 호출했는지, 함수로서 호출했는지 판단한다.
🚨 어떤 함수를 함수로서 호출하면 실행 컨텍스트가 생성될 때
this
바인딩을 따로 하지 않는다.
실행 컨텍스트 활성화 당시에 this
가 지정되지 않은 경우 this
에는 전역 객체가 저장된다.
전역 객체에는 브라우저의 window
, Node.js의 global
객체 등이 있다.
이 글을 읽고 서로선배와 결혼을 결심했습니다.
감사합니다.