너굴 개발 일지

[리뷰] 코어 자바스크립트 2장 - 실행 컨텍스트 본문

독서

[리뷰] 코어 자바스크립트 2장 - 실행 컨텍스트

너굴냥 2023. 6. 3. 21:05

이 글은 코어 자바스크립트 책을 읽으며 정리한 글입니다.

 

2장. 실행 컨텍스트

2-1. 실행 컨텍스트

실행 컨텍스트란 실행할 코드에 제공할 환경 정보들을 모아놓은 객체로 다음과 같은 방식으로 진행되며 전체 코드의 환경과 순서를 보장합니다.

  1. 동일한 환경에 있는 코드들을 실행할 때, 필요한 환경 정보를 모아 컨텍스트를 구성
  2. 이를 콜스택에 쌓아 올림
  3. 가장 위에 쌓여 있는 컨텍스트와 관련 있는 코드들을 실행

보통 실행 컨텍스트를 구성하는 방법은 함수를 실행하는 것 뿐입니다. (자동으로 생성되는 전역 공간과 eval 제외)

참고로 최상단의 공간은 코드 내부에서 별도의 실행 명령 없이도 브라우저에서 자동으로 실행하므로 JS 파일이 열리는 순간 전역 컨텍스트가 활성화됩니다.

 

실행 컨텍스트에 담기는 정보들은 다음과 같습니다.

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

VariableEnvironment, LexicalEnvironment는 environmentRecord, outerEnvironmentReference 2가지로 구성됩니다. 

2-2. Variable Environment

LexicalEnvironment와 차이점은 최초 실행시의 스냅샷을 유지합니다.

실행 컨텍스트 생성시 VariableEnvironment에 정보를 먼저 담은 후 그대로 복사해 LexicalEnvironment를 생성합니다. 이후에는 LexicalEnvironment를 주로 활용합니다.

2-3. Lexial Environment

(1) 환경 레코드와 호이스팅

호이스팅 규칙

환경 레코드란?

  • 현재 컨텍스트와 관련된 코드의 식별자 정보들이 저장됩니다. (함수 매개변수 식별자, 선언한 함수 그 자체, var로 선언된 변수 등의 식별자)
  • JS 엔진이 컨텍스트 내부 전체를 처음부터 끝까지 스캔하며 순서대로 식별자 정보를 수집합니다.
  • 코드가 실행되기 전이지만, 코드의 변수명 수집이 완료되기에 JS 엔진은 식별자를 최상단으로 끌어올려 놓은 후 실제 코드를 실행한다고 간주할 수 있으며 이를 호이스팅이라는 개념으로 칭합니다.

 

매개변수와 변수에 대한 호이스팅(1) - 원본 코드

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

위 코드처럼 인자들과 함께 함수를 호출한 경우의 동작을 보면, arguments에 전달된 인자를 담는 것을 제외하면 아래 코드처럼 코드 내부에서 변수를 선언한 것과 다른 점이 없습니다. 특히 LexicalEnvironment 입장에서는 완전히 같기에 인자를 함수 내부의 다른 코드보다 먼저 선언 및 할당이 이뤄진 것처럼 간주할 수 있습니다.

 

arguments란?

더보기

특정 함수의 실행 컨텍스트 구성시, arguments(매개변수)도 함께 만들어지는데, 전달한 인자가 모두 arguments 정보에 담깁니다.

따라서 인자를 함수 내부의 최상단에 선언 및 할당이 이뤄진 것으로 간주할 수 있습니다. 

 

매개변수와 변수에 대한 호이스팅(2) - 매개 변수를 변수 선언/할당과 같다고 간주해서 변환한 상태

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();

환경 레코드는 현재 실행될 컨텍스트 대상 코드 내에 어떤 식별자들이 있는지에만 관심이 있기에 변수 호이스팅시 변수명만 끌어올리고 할당부는 원래 자리에 그대로 남겨둡니다.

 

매개변수와 변수에 대한 호이스팅(3) - 호이스팅을 마친 상태

function a() {
  var x;          // 수집 대상 1의 변수 선언 부분
  var x;          // 수집 대상 2의 변수 선언 부분
  var x;          // 수집 대상 3의 변수 선언 부분

  x = 1;          // 수집 대상 1의 할당 부분
  console.log(x); // (1)
  console.log(x); // (2)
  x = 2;          // 수집 대상 3의 할당 부분
  console.log(x); // (3)
}
a();

처음 예측 결과는 (1) 1, (2) undefined, (3) 2로 출력되리라 예상했지만, 실제로는 (1) 1, (2) 1, (3) 2라는 결과가 나왔습니다.

이는 호이스팅으로 인해 예측한 것과 다른 결과가 나온 것입니다.

 

함수 선언의 호이스팅 (1) - 원본 코드

function a() {
  console.log(b);   // (1)
  var b = "bbb";    // 수집대상 1(변수 선언)
  console.log(b);   // (2)
  function b() {};  // 수집 대상 2(함수 선언)
  console.log(b); // (3)
}
a();

a 함수를 실행하는 순간 a 함수의 실행 컨텍스트가 생성됩니다. 이때 변수명과 함수 선언의 정보를 위로 끌어올립니다(수집합니다).

변수는 선언부와 할당부를 나누어 선언부만 끌어올리지만 함수 선언은 함수 전체를 끌어올립니다. (JS 창시자가 JS를 유연한 언어로 만들기 위함)

 

 

함수 선언의 호이스팅 (2) - 호이스팅을 마친 상태 (함수 선언문을 함수 표현식으로 바꾼 코드)

function a() {
  var b; // 수집 대상 1. 변수는 선언부만 끌어올림
  var b = function b() {}; // 바뀐 부분

  console.log(b); // (1)
  b = "bbb"; // 변수의 항당부는 원래 자리에 남겨둠
  console.log(b); // (2)
  console.log(b); // (3)
}
a();

호이스팅이 끝난 상태에서의 함수 선언문은 함수명으로 선언한 변수에 함수를 할당한 것처럼 여길 수 있기에 함수 선언문을 함수 표현식으로 변경했습니다.

호이스팅을 고려하지 않았을 땐, (1) 에러 또는 undefined, (2) 'bbb', (3) b 함수가 나올 것 같지만 실제로는 (1) b 함수, (2) 'bbb', (3) 'bbb'라는 전혀 다른 결과가 나왔습니다.

 

 

함수 선언문과 함수 표현식

함수를 정의하는 방법은 다음과 같습니다.

  1. 함수 선언문 : function 정의부만 존재하며 별도의 할당 명령은 없습니다.
  2. 함수 표현식: 정의한 function을 별도의 변수에 할당합니다.
    1. 기명 함수 표현식 : 함수명을 정의한 함수 표현식
    2. 익명 함수 표현식 : 함수명을 정의하지 않은 함수 표현식
function a() { } // 함수 선언문, 함수명 a가 곧 변수명
a(); // 실행 O

var b = function () { } // (익명) 함수 표현식, 변수명 b가 곧  함수명
b(); // 실행 O

var c = function d() { } // (기명) 함수 표현식, 변수명은 c, 함수명은 d
c();  // 실행 O
d();  // 에러

기명 함수 표현식을 사용할 경우, 외부서 함수명으로 함수를 호출할 수 없으며 오직 내부에서만 접근 가능합니다.

따라서 내부서 재귀 함수를 호출하는 용도로 사용할 수 있지만 변수명으로 호출이 가능하기에 굳이 함수명으로 호출할 필요가 있을지는 의문입니다.

 

함수 선언문과 함수 표현식의 차이

console.log(sum(1, 2)); // 3
console.log(mutiply(1, 2)); // TypeError: mutiply is not a function

function sum(a, b) {
  return a + b;
}

var mutiply = function (a, b) {
  return a * b;
};

함수 선언문은 전체를 호이스팅하지만 함수 표현식은 변수 선언부만 호이스팅됩니다. 이로써 함수도 하나의 값으로 취급할 수 있습니다.

sum 함수는 선언 전에 호출해도 문제 없이 실행되기에 오류 가능성이 적은 함수 선언문이 더 나을 수도(?) 있다고 생각할 수 있겠지만 혼란을 야기할 수 도 있습니다.

전역 컨텍스트가 활성화 될 때, 전역 공간에 선언된 모든 함수들이 최상단으로 끌어올려지기에 동일한 변수명에 서로 다른 값을 할당한 경우, 나중에 할당한 값이 먼저 할당한 값을 덮어씌웁니다. 따라서 원활한 협업을 위해선 전역 공간 함수 선언 혹은 동명 함수 중복 선언을 피하는 것이 좋습니다. 혹은 동명 함수가 여럿 존재해도 모든 함수를 함수 표현식으로 작성하는 것이 좋습니다.

 

(2) 스코프, 스코프 체인, outerEnvironmentReference

스코프란 식별자에 대한 유효범위를 의미하며 어떤 경계의 A의 내부에 선언한 변수는 오직 A 내부에서만 접근 가능합니다.

ES5까지는 전역 공간을 제외하면 함수에 의해서만 스코프가 생성되었지만 ES6부터는 블록에 의해서도 스코프 경계가 생성됩니다.

블록 스코프는 var로 선언한 변수에는 작용하지 않고 오직 let, const, class, strict mode에서의 함수 선언 등에 대해서만 범위로서 역할을 수행합니다.

 

스코프 체인

스코프 체인이란 '식별자의 유효 범위'를 안에서부터 바깥으로 차례로 검색해 나가는 것을 의미합니다. LexicalEnvironment (이하 L.E)의 outerEnvironment (이하 외부 환경 참조)로 인해 가능한 일입니다.

외부 환경 참조는 현재 호출된 함수가 선언될 당시의 L.E를 참조합니다. 

여기서 선언 시점이란, 콜스택 상에서 어떤 실행 컨텍스트가 활성화된 상태일뿐입니다.

 

스코프 체인 예시

var a = 1;					// (1)
var outer = function () {			// (2)
  var inner = function () {			// (3)
    console.log(a);				// (4)
    var a = 3;					// (5)
  };						// (6)
  inner();					// (7)
  console.log(a);				// (8)
};						// (9)
outer();					// (10)
console.log(a);					// (11)

위 흐름처럼 외부 환경 참조는 연결 리스트 형태를 띄며, 선언 시점의 L.E를 계속 찾아 올라가면 마지막엔 전역 컨텍스트의 L.E가 있게 됩니다. 이런 구조적 특성으로, 여러 스코프에 동일한 식별자를 선언한 경우 무조건 스코프 체인 상에서 가장 먼저 발견된 식별자에만 접근할 수 있습니다.

inner 함수 내부에서 식별자 a에 접근시 스코프 체인 상에서 가장 가까운 inner 스코프에 있는 L.E의 a를 반환하였으며 전역 공간에 선언한a에는 접근할 수 없게 됐는데 이를 활용한 개념을 변수 은닉화라고 합니다.

 

 

전역 변수, 지역 변수

  • 전역 변수 : 전역 공간에 선언된 변수로 위 코드에선 a, outer가 해당됩니다.
  • 지역 변수 : 함수 내부에서 선언한 변수로 위 코드에선 inner, inner 내부에서 선언한 a가 해당됩니다.

 

2-4. this

실행 컨텍스트의 thisBinding에는 this로 지정된 객체가 저장되며, this가 지정되지 않았다면 전역 객체가 저장됩니다.

 

2-5. 정리

실행 컨텍스트란?

  • 코드를 실행하는데 필요한 환경 정보를 저장한 객체
  • 전역 공간에서 자동으로 생성되는 전역 컨텍스트와 eval 및 함수 실행에 의한 컨텍스트 등이 존재
  • 실행 컨텍스트 객체가 활성화되는 시점에 VariableEnvironment, LexicalEnvironment, ThisBinding의 세 가지 정보를 수집

VariableEnvironment, LexicalEnvironment 개념과 차이점?

  • VariableEnvironment, LexicalEnvironment 모두 실행 컨텍스트의 식별자 정보 + 외부 환경 정보를 저장하는 역할
  • 차이점으로는 LexicalEnvironment은 함수 실행 도중에 변경되는 사항이 즉시 반영되지만, LexicalEnvironment는 초기 상태를 유지
  • 두 가지 모두 함수의 매개변수 인자, 변수 식별자, 선언한 함수의 함수명 등을 수집하는 environmentRecord와 바로 직전 컨텍스트의 LexicalEnvironment 정보를 참조하는 outerEnvironmentReference로 구성

호이스팅이 개념과 변수/함수에서의 차이점, 함수 선언문과 함수 표현식에서의 차이점?

  • 코드 실행 전 변수 선언문 및 함수 그 자체가 코드 최상단으로 끌어 올려지는 것처럼 보여지는 현상을 의미하며, 이는 environmentRecord의 수집 과정을 추상화한 개념
  • 변수 선언과 값 할당이 동시에 이뤄진 문장은 '선언부'만을 호이스팅합니다.
  • 함수 선언문은 함수 전체가 호이스팅되며 함수 표현식에서는 변수 선언만 호이스팅됩니다.

스코프란?

  • 식별자의 유효 범위를 의미합니다.

전역 변수, 지역 변수란?

  • 전역 변수란 전역 환경에서 선언된 변수를 의미합니다.
  • 지역 변수란 함수 내부에서 선언된 변수를 의미합니다.

this란?

  • 실행 컨텍스트를 활성화하는 당시에 지정된 this가 저장되며, this를 지정하지 않은 경우 전역 객체가 저장돱니다.

 

 


Quiz

호이스팅 개념과 발생하는 원인은 무엇일까요?

더보기

호이스팅이란 선언 라인 전에도 에러 없이 변수를 참조할 수 있는 현상으로 자바스크립트 엔진은 소스 코드 실행 전 전체 코드를 스캔하며 콜 스택에 있는 전역 실행 컨텍스트의 환경 레코드에 식별자를 기록하기 때문입니다.

실행 컨텍스트를 구성하는 세 가지 컴포넌트는 무엇인가요? 그리고 각 역할은 무엇인가요?

더보기

3가지 컴포넌트는 Variable Enviroment, , LexicalEnvironment, ThisBinding가 있으며 Variable Enviroment는 최초 실행 시의 스냅샷을 기록하고 Lexical Environment은 코드가 실행되며 식별자의 정보가 변화되는 정보를 기록하게 됩니다.

 

아래 코드 실행시 콘솔창에 출력되는 결과값과 이유는 무엇인가요?

var x = 1;

function foo() {
    var x = 10;
    bar();
}

function bar() {
    console.log(x);
}

foo();  // ?
bar();  // ?
더보기

둘 다 1로 출력되는데 bar 함수의 실행 컨텍스트에는 x가 존재하지 않기에 outerEnvironmentReference를 통해 외부 렉시컬 환경에서 찾게 됩니다.

foo, bar 함수 모두 전역에서 정의된 전역 함수기에 어디서 호출되어도 상위 스코프는 항상 전역을 가리킵니다.

따라서 bar 함수의 outerEnvironmentReference도 외부 렉시컬 환경인 전역을 참조하기에 전역 실행 컨텍스트에 있는 x=1, 즉 1을 출력하게 됩니다.

 


관련 글

https://github.com/raccoon-ccoder/TIL/blob/main/JavaScript/%5BExecution%20Context%5D%20%EC%8B%A4%ED%96%89%20%EC%BB%A8%ED%85%8D%EC%8A%A4%ED%8A%B8%20%26%20%EB%A0%89%EC%8B%9C%EC%BB%AC%20%ED%99%98%EA%B2%BD%20(record%2C%20outer).md

 

GitHub - raccoon-ccoder/TIL: Today I Learned

Today I Learned. Contribute to raccoon-ccoder/TIL development by creating an account on GitHub.

github.com

https://github.com/raccoon-ccoder/TIL/blob/main/JavaScript/%5B%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%20%EC%84%A0%EC%96%B8%20%EB%B0%A9%EC%8B%9D%5D%20var%2C%20let%2C%20const.md