Books
코어 자바스크립트
2. 실행 컨텍스트

02 실행 컨텍스트


2.1 실행 컨텍스트란?

실행 컨텍스트는 실행할 코드에 제공할 환경 정보를 모아놓은 객체이다. 하나의 실행 컨텍스트를 구성할 수 있는 방법으로는

  1. 전역공간
  2. eval() 함수
  3. 함수

우리가 흔히 실행 컨텍스트를 구성하는 방법을 함수를 실행하는 것 뿐이다.

// ---------------------- (1)
var a = 1;
function outer() {
  function inner() {
    console.log(a); // undefined
    var a = 3;
  }
  inner();  // ---------- (2)
  console.log(a); // 1
}
outer();  // ------------ (3)
console.log(a); // 1

처음 코드를 실행하는 순간(1) 전역 컨텍스트가 콜 스택에 담긴다. 최상단의 공간은 별도의 실행 명령 없이도 브라우저에서 자동으로 실행하기 때문에 파일이 열리는 순간 전역 컨텍스트가 활성화된다고 생각하면 된다. (3)에서 outer 함수를 호출하면 엔진은 outer에 대한 환경 정보를 수집해서 실행 컨텍스트를 생성한 후 콜 스택에 담는다. (2)에서 inner 함수의 실행 컨텍스트가 콜 스택에 담긴다.

실행 컨텍스트 객체에 담기는 정보는

  • VariableEnvironment: 현재 컨텍스트 내의 식별자들에 대한 정보 + 외부 환경 정보, 선언 시점의 LexicalEnvironment의 스냅샵으로 변경사항은 반영되지 않음.
  • LexicalEnvironment: 처음에는 VariableEnvironment와 같지만 변경 사항이 실시간으로 반영됨.
  • ThisBinding: this 식별자가 바라봐야 할 대상 객체

1.2 VariableEnvironment

실행 컨텍스트를 생성할 때 VariableEnvironment에 정보를 먼저 담고, 이를 복사해서 LexicalEnvironment를 만들고, 이후에는 LexicalEnvironment를 주로 활용한다. 내부는 environmentRecord와 outer-EnvironmentReference로 구성되어 있다.

1.3 LexicalEnvironment

lexical environment는 "컨텍스트를 구성하는 환경 정보들을 사전에서 접하는 느낌으로 모아놓은 것"

2.3.1 environmentRecord와 호이스팅

environmentRecord에는 현재 컨텍스트와 관련된 코드의 식별자 정보들이 저장된다. 컨텍스트를 구성하는 함수에 지정된 매개변수 식별자, 선언한 함수가 있으면 그 함수 자체, var로 선언된 변수의 식별자 등 컨텍스트 내부 전체를 처음부터 끝까지 순서대로 수집한다.

💡

전역 실행 컨텍스트는 변수 객체를 생성하는 대신 JS 런타임 환경이 제공하는 객체 (전역 객체)를 활용한다. 브라우저의 window나 nodejs의 global 등이 있고, 이들은 JS 내장 객체가 아닌 호스트 객체로 분류된다.

코드가 실행되기 전에 이미 엔진은 해당 환경에 속한 코드의 변수명들을 알고 있게 된다. 엔진이 식별자들을 최상단으로 끌어올려놓은 다음 코드를 실행한다 라고 생각해도 해석하는데 문제될것이 없다. 이를 **호이스팅(hoisting)**이라고 하는데, 엔진이 실제로 끌어올리지는 않지만 편의상 끌어올린 것으로 간주한다는 것이다.

호이스팅 규칙

function a() {
  var x = 1;  // 수집 대상 1
  console.log(x); // (1)
  var x;      // 수집 대상 2
  console.log(x); // (2)
  var x = 2;  // 수집 대상 3
  console.log(x); // (3)
}
a();

이 상태에서 변수 정보를 수집하는 과정, 호이스팅을 처리해보면, environmentRecord는 현재 실행될 컨텍스트의 어떤 식별자들이 있는지만 관심있고 어떤 값이 할당될 것인지는 관심이 없다. 따라서 변수명만 끌어올리고 할당 과정은 그대로 둔다.

function a() {
  var x;  // 수집 대상 1
  var x;
  var x;
  x = 1;
  console.log(x); // (1)
  console.log(x); // (2)
  x = 2;  // 수집 대상 3
  console.log(x); // (3)
}
a();

이로써 실제 콘솔의 출력은 1, 1, 2가 된다.

function a() {
  console.log(b);
  var b = 'bbb';
  console.log(b);
  function b() {}
  console.log(b);
}
a();

위 코드는

function a() {
  var b;
  var b = function b() {}
  console.log(b);
  b = 'bbb';
  console.log(b);
  console.log(b);
}
a();

이렇게 변하고 콘솔 출력에는 b함수, 'bbb', 'bbb'가 나온다.

함수 선언문과 함수 표현식

  • 함수 선언문 (function declaration) : function 정의부만 존재하고 별도의 할당 명령이 없다.
    • 반드시 함수명이 정의돼 있어야 한다.
  • 함수 표현식 (function expression) : 정의한 function을 별도의 변수에 할당한다.
    • 함수명이 없어도 된다.
function a() {}         // 함수 선언문
a();  // ok
 
var b = function () {}  // (익명) 함수 표현식
b();  // ok
 
var c = function d() {} // 기명 함수 표현식
c();  // ok
d();  // error
console.log(sum(1, 2));
console.log(multiply(3,4));
function sum (a,b) {
  return a + b;
}
var multiply = function (a,b) {
  return a * b;
}

위 코드가 environmentRecord의 정보 수집 과정에서 호이스팅 과정을 마치면

var sum = function sum(a,b) { // 함수 선언문은 전체를 호이스팅 한다.
  return a + b;
}
var multiply;
console.log(sum(1,2));
console.log(multiply(3,4));
multiply = function (a,b) { // 변수의 할당부는 원래 자리에 남겨둔다.
  return a * b;
}

sum함수는 선언 전에 호출해도 아무 문제 없이 실행된다. 이 부분이 어색할 수 있는데 호이스팅을 이해하더라고 거부감이 느껴질 수 있다. '선언 후에 호출할 수 있다'라고 이해하는 편이 훨씬 자연스러우니까. 또 이 때문에 실무에서 버그를 만들 수 있다. 그래서 상대적으로 함수 표현식이 안전하다.

2.3.2 스코프, 스코프 체인, outerEnvironmentReference

스코프란 식별자에 대한 유효범위이다. ES5까지의 자바스크립트는 전역공간을 제외하면 오직 함수에 의해서만 스코프가 생성된다. 식별자의 유효범위를 안에서부터 바깥으로 차례대로 검색하는 것을 스코프 체인이라고 한다. 그리고 이게 가능한 이유는 outerEnvironmentReference 때문이다.

스코프 체인 outerEnvironmentReference는 현재 호출된 함수가 선언될 당시의 LexicalEnvironment를 참조한다. '선언될 당시'라는게 중요하다. 여러 스코프에서 동일한 식별자를 선언한 경우에는 무조건 스코프 체인 상에서 가장 먼저 발견된 식별자에만 접근 가능하게 된다.

var a = 1;
var outer = function () {
  var inner = function () {
    console.log(a);
    var a = 3;
  };
  inner();
  console.log(a);
};
outer();
console.log(a);
  • 전역 컨텍스트의 LexicalEnvironment는 a, outer
  • outer 컨텍스트의 LexicalEnvironment는 inner, outerEnvironmentReference는 전역 LE
  • inner 컨텍스트의 LE는 a, outerEnvironmentReference는 outer LE
  1. innerconsole.log(a)undefined
  • inner의 LE에서 a가 있기 때문에 검색을 종료하고, 아직 값이 할당되기 전이기 때문에 undefined를 출력한다.
  1. outerconsole.log(a)는 1
  • outer의 LE에는 a가 없기 때문에, outerEnvironmentReference에서 a를 찾고 그 값인 1이 출력됨
  1. 전역 컨텍스트에서 console.log(a)는 1
  • 전역 LE에서 a를 찾았고 그 값인 1을 출력

전역변수와 지역변수

위 예제에서 전역변수는 aouter 이다. 지역변수는 outer 내부에서 선언한 innerinner 내부에서 선언한 a이다.

2.4 this

실행 컨텍스트의 thisBinding에는 this로 지정된 객체가 저장된다. 실행 컨텍스트 활성화 당시 this가 지정되지 않으면 this는 전역 객체가 저장된다.