코어 자바스크립트를 읽고 스터디 세션에서 공유를 위해 정리한 글입니다.

📌 실행 컨텍스트(execution context)는 실행할 코드에 제공할 정보들을 모아놓은 객체다.

  • JS는 어떤 실행 컨텍스트가 활성화 되는 시점에 변수를 끌어올리고(hoisting), 외부 환경 정보를 구성하고, this 값을 설정하는 등의 동작을 수행한다.
  • 클로저를 지원하는 대부분의 언어에 비슷한 개념이 적용되어 있다.

1. 실행 컨텍스트란?

  • 앞서 실행 컨텍스트를 실행할 코드에 제공할 환경 정보를 모아놓은 객체라고 소개했다.
  • 동일한 환경에 있는 코드들을 실행할 때 필요한 환경 정보들을 모아 컨텍스트를 구성하고 이를 콜 스택(call stack)에 쌓아올렸다가, 가장 위에 있는 컨텍스트와 관련 있는 코드들을 실행하는 식으로 전체 코드의 환경과 순서를 보장한다.

📌 여기서 ‘동일한 환경’, 즉 실행 컨텍스트를 구성할 수 있는 방법

  • 전역공간
  • eval() 함수
  • 함수
  • 블록{} → ES6 부터…
/* -------------------------------------------------------------------------- */
/*                                     (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) 전역 컨텍스트가 활성화된다.
  • 전역 컨텍스트는 일반 컨텍스트와 다를 것이 없다.(다만, 최상단의 공간은 별도의 실행 명령이 없어도 브라우저에서 자동으로 실행하기 때문에, 이후에 설명할 arguments가 없다. 전역 공간을 둘러싼 외부 스코프가 존재할 수 없기 때문에 스코프 체인상에는 전역 스코프 하나만 존재한다.)

📌 전역 컨텍스트와 다른 컨텍스트의 차이점에 대해 이야기해 봐라.
- 함수 호출이나 eval을 이용해 생성되는 컨텍스트가 아니라 JS파일 실행 시 자동으로 생성되기 때문에 arguments가 존재하지 않는다.

  • 전역 공간을 둘러싼 외부 스코프가 존재할 수 없어 스코프 체인상에는 전역 스코프 하나만 존재한다.

실행 순서 - 이후 전역 컨텍스트 외에 다른 덩어리가 없기 때문에 전역 컨텍스트와 관련된 코드를 순차적으로 진행 하다가 `(3)에서 outer함수를 호출하면 전역 컨텍스트와 관련된 코드를 중지하고 JS엔진에서 outer와 관련된 환경 정보를 수집해 실행 컨텍스트를 생성 후 콜 스택에 담는다`.
- 다시 (2)에서 inner 함수를 호출하면 outer 컨텍스트와 관련된 코드 중지 → inner 함수 내부의 코드를 실행
- 이후 a 변수에 3 할당 → inner 함수 종료, inner의 실행 컨텍스트가 콜스택에서 제거된다.
- (2) 다음 줄부터 이어서 실행.
- outer 함수 종료 후 (3) 다음 코드에서 다시 실행 및 a 값 출력 후 종료
- 어느 컨텍스트가 콜 스택 제일 위에 쌓이는 순간 JS 엔진이 해당 컨텍스트에 관련된 코드들을 실행하는데 필요한 환경 정보들을 수집해서 실행 컨텍스트 객체에 저장한다.
- 엔진이 활용할 목적으로 생성하는 정보기 때문에 사용자가 코드적으로 확인할 수는 없다.

📌 실행 컨텍스트가 수집하는 정보

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

활성화된 실행 컨텍스트의 수집 정보

2. Variable Environment

  • Variable Environment에 담기는 내용은 Lexical Environment와 같지만 최초 실행 시의 스냅샷을 유지한다는 점에서 다르다.
  • 실행 컨텍스트를 생성할 때 Variable Enviroment에 정보를 담은 이후 이를 복사해 Lexical Environment를 만들고 이후에는 Lexical Environment를 주로 이용한다.
    관련 문서
  • environmentRecord와 outer-EnvironmentReference로 구성

3. Lexical Environment

📌 저자가 생각하는 Lexical의 의미…(참고만 하세요)

Lexical Environment의 한글 번역이 다양함. ‘어휘적, 정적 환경 등…’ → 어휘적은 사전적 표상적이지 않아 의미가 와닿지 않음. 정적은 시질적으로 환경이 계속 변하기 때문에 적절한 번역이 아니라고 생각함.
그래서 저자는 사전적인이 어울리는 의미라고 생각함.
컨텍스트를 구성하는 환경 정보들을 사전에서 접하는 느낌으로 모아놓은 환경이기 때문에…
→ 책에서는 여러 번역이 존재하는 한글이 아닌, 타인과의 커뮤니케이션을 원할히 할 수 있는 원어를 사용할 것임.

2-3-1. enviromentRecord와 호이스팅

  • enviromentRecord는 현재 컨텍스트와 관련된 코드의 식별자 정보들이 저장된다.
    • 컨텍스트를 구성하는 함수에 지정된 매개변수 식별자
    • 선언한 함수가 있을 경우 그 함수 자체
    • var로 선언된 변수의 식별자
  • 컨텍스트 내부 전체를 처음부터 훑어가며 순서대로 수집한다.
  • 변수 정보를 수집하는 과정을 완료했더라도 실제 코드 실행을 위해 실행 컨텍스트가 관여할 코드는 실행되기 전의 상태이다. 즉, 코드가 실행되기 전임에도 불구하고 JS엔진은 해당 환경에 속한 코드의 변수명을 모두 알고있게 된다.
    → 여기에서 호이스팅(hoisting)이라는 개념이 나온다.
  • 위에서 설명한 JS엔진의 실행 컨텍스트 정보 수집 동작 설명 대신, JS 엔진은 식별자들을 최상단으로 올려놓은 다음 실행한다.라고 설명하는 이해들 돕기위해 가상 개념인 호이스팅으로 대체해도 해도 이해에는 무리가 없을 것이다…
    → 실제로 그렇게 동작은 X!

📌 전역 실행 컨텍스트

  • 변수 객체를 생성하는 대신 런타임 환경이 별도로 제공하는 객체(전역 객체)를 사용한다.
  • 브라우저: window, Node.js: global 객체 등…
  • 내장 객체(native object)가 아닌 호스트 객체(host object)로 분류된다.

호이스팅 규칙

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

  console.log(x); // (2)
  var x = 2; // 수집 대상 3(변수 선언)

  console.log(x); // (2)
}

a(1);

호이스팅 개념을 적용하지 않았을 때의 예상되는 출력 값

(1) → 1
(2) → undefined (변수 선언 후 값을 할당하지 않았기 때문)
(3) → 2

📌 주의
이제부터 실제 동작을 대체해 설명하기 위한 호이스팅 개념을 적용한 코드로 동작을 설명 함. 실제 엔진에서 이러한 코드적 변환을 거치지는 않는다.

  • 다음 코드는 arguments에 전달된 인자를 담는다는 것을 제외하면 코드 내부에서 변수를 선언한 것과 다른 점이 없다. (특히 LexicalEnviroment 입장에서는 완벽히 동일).그렇기 때문에 인자를 함수 내부의 다른 코드보다 먼저 선언 및 할당한 것으로 간주할 수 있다.
function a() {
  var a = 1; // 수집 대상 1(매개변수 선언)
  console.log(a); // (1)

  var a; // 수집 대상 2(변수 선언)
  console.log(a); // (2)

  var a = 2; // 수집 대상 3(변수 선언)
  console.log(a); // (3)
}
📌 arguments란? - 실행 컨텍스트 생성 시점에 함께 만들어지는 정보 중 하나
- 지정한 매개변수의 개수와는 무관하게 호출 시 전달한 인자가 모두 저장된다.
- 기존에는 함수의 자율성을 높이기 위해 arguments를 광범위하게 사용했으나, 유사배열 객체라서 배열처럼 사용하기 위해 별도의 처리가 필요했다.
- 그리고 함수 내부에서 매개변수의 값을 바꾸면 arguments의 값도 변한다. 이는 ‘전달된 인자를 모두 저장하는 데이터’라는 본래의 의미가 퇴색된다.
- 이런 문제를 해결하는 나머지 파라미터(rest parameter, ES6)가 등장함. 나머지 파라미터는 arguments를 온전히 대신할 수 있다.
  • enviromentRecord는 현재 실행될 컬텍스트의 대상 코드 내에 어떤 식별자가 존재하는지에만 관심이 있다.(할당될 값에 대해서는 관심 x)
    → 호이스팅 할 변수명만 끌어올린다. 매개변수도 마찬가지다.
function a() {
  var a; // 수집 대상 1의 변수 선언 부분
  var a; // 수집 대상 2의 변수 선언 부분(메모리 변수 공간에 이미 식별자가 존재함 무시)
  var a; // 수집 대상 3의 변수 선언 부분(메모리 변수 공간에 이미 식별자가 존재함 무시)

  a = 1; // 수집 대상 1의 변수 선언 부분
  console.log(a); // (1)
  console.log(a); // (2)

  a = 2; // 수집 대상 3의 변수 선언 부분
  console.log(a); // (3)
}
  • 동작 흐름
    - 2번 라인에서 변수영역에 식별자를 a로하는 공간을 잡아놓는다.
    - 3, 4는 이미 같은 식별자를 가진 공간이 존재하니 무시.
    - a에 1 할당 후 (1),(2) 실행, a에 2할당 후 (3) 실행.
  • 처음에 호이스팅을 적용하지 않고 예상한 (1) 1, (2) undefined, (3) 2가 아닌 (1) 1, (2) 1, (3) 2 출력.
  • 호이스팅 개념을 이해하지 못하면 (2)번의 1을 예측하기 힘듦.
// 호이스팅 전
function a() {
  console.log(b);
  
  var b = "bbb";
  console.log(b);

  function b() {
  }
  console.log(b);
}

// => 예상 결과: undefined나 error | bbb | 함수 b

// 호이스팅 후
function a() {
  var b;
	var b = function b() { }
  
  console.log(b);

  b = "bbb";
  console.log(b);
  console.log(b);
}

// => 실제 결과: 함수 b | bbb | bbb

함수 선언문과 함수 표현식

개요

  • 함수 선언문: function 정의부만 존재, 별도의 할당 명령은 없다.
    • 함수명이 반드시 정의되어야 한다.
  • 함수 표현식: 정의한 function을 별도의 변수에 할당하는 것을 말한다.
    • 익명 함수 표현식, 기명함수 표현식 둘 다 가능
  • 예제
    function a() { // 함수명 a
    }
    
    const b = function () {
    }
    
    const c = function d() { // 변수명은 c, 함수명은 d
    }
    
    c(); // 실행 OK!
    d(); // 에러
  • 기명 함수 표현식은 외부에서 함수명으로 호출할 수 없다. 함수명으로는 함수 내에서만 접근 가능.
  • 기명 함수 표현식의 용도: 과거에는 익명 함수 표현식은 undefined, unnamed라는 값이 나왔다. → 그래서 기명 함수 표현식이 디버깅에 이점이 있었다. 그러나 요즘 브라우저들은 모두 익명 함수의 name 프로퍼티에 변수명을 할당해준다.
// 호이스팅 전
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;
}

// 호이스팅 후
var sum  = function sum(a,b){
	return a + b;
}
var multiply;

console.log(sum(1,2)); // 3
console.log(multiply(3,4)); // multiply is not a function

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

❗ 함수 선언식의 위험성

  • 함수 선언식은 초보 개발자의 접근성에는 좋을 수도 있다…
  • 그러나 협업에 있어서 위험할 수 있다.(극단적 예시)

console.log(sum(10,3));

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

// 10만년 전까지 시니어 개발자 수달이 여기까지 코드를 짜고 라인 2에서 원하는 결과(5)를 잘 얻어냈다.
// 10만년 후 현재 신입 개발자 해달이 코드를 짜기 시작했는데,
// 기존의 sum이 존재하는지 모르고 코드라인 10만에 문자열로 식을 이쁘게 보여주는 함수(sum)를 짰다.
 
// 코드라인 10만
function sum(a,b){
	return `${a} + ${b} = ${a+b}`;
}

console.log(sum(10000, 20000)); // 10000 + 20000 = 30000
// 함수가 호이스팅 되어 라인 2의 결과도 해달이 구현한 함수의 내용대로 동작한다...
// 디버깅하기도 쉽지 않다.

2-3-2 스코프, 스코프 체인, outerEnvironmentReference

스코프란 식별자에 대한 유효범위이다.

  • ES5 까지는 전역 공간을 제외하면 오직 함수에 의해서만 스코프가 생성된다.
  • ES6에서는 블록에 의해서도 스코프가 생성된다().
    • var가 아닌 let, const, class, strict mode에서의 함수 선언 등에 대해서만 범위로서의 역할을 수행할 수 있다.
  • 이 둘을 구분하기 위해 블록 스코프, 함수 스코프라는 용어 사용.

스코프 체인

  • outerEnvironmentReference는 현재 호출된 함수가 선언될 당시의 LexicalEnvironment를 참조한다. → 선언된다라고 하는 시점이란 콜 스택 상에 이미 어떤 실행 컨텍스트가 활성화 된 상태이다. → 어떤 함수를 선언하는 행위도 코드에 지나지 않으며 모든 코드는 실행 컨텍스트가 활성화 사태일 때 실행되기 때문이다(적어도 전역 컨텍스트는 활성화 되어있을 것이다.).
  • 만약 A 함수 내부에 B 함수, B함수 내부에 C함수를 선언 → C의 outerEnvironmentReference → B의 LexicalEnvironment, B의 outer… → A의 Lexical…, A의 o → 전역 컨텍스트의 LE 참조
  • 내부 함수에서 외부를 향해가는 링크드 리스크 형식으로 가까운 요소로부터 단계적으로만 접근할 수 있다. → 이런 구조적 특징 덕에 스코프 체인 상에서 가장 먼저 발견된 식별자만 접근 가능.
var a = 1;
var outer = function () {
	var inner = function () {
		console.log(a); // undefined
		var a = 3;
	}
	inner();
	console.log(a); // 1
}
outer();
console.log(a); // 1

  • 전역 컨텍스트 → outer 컨텍스트 → inner 컨텍스트 순으로 규모는 작아지고 접근 가능한 변수의 수는 늘어남(한 레벨 위의 컨텍스트만 참조 가능하다!)
  • inner는 먼저 스코프 체이닝에서 제일 먼저 자신을 컨텍스트를 보고 a가 존재하면 탐색을 종료한다. → global의 a에는 접근 불가하다. 이를 변수 은닉화라고 한다.

참고: 스코프 체인 확인하는 방법

console.dir 디버거 이용

전역변수와 지역변수

  • 전역변수: 전역 공간에 선언한 변수
  • 지역변수: 함수 내부에서 선언한 변수
  • 전역변수의 사용을 최소화하는 방법

this

  • this를 따로 지정하지 않는 경우는 전역객체 저장 → 3장에서 자세히 소개

정리

  • 실행 컨텍스트의 종류
    • 전역 컨텍스트
    • eval, 함수 실행에 의한 컨텍스트
  • 실행 컨텍스트가 수집하는 정보
    • VariableEnvironment(초기 상태 유지)
    • LexicalEnvironment(변경 사항이 생기면 즉시 반영)
      • EnvironmentRecord: 매개변수명, 변수 식별자, 선언한 함수의 함수명
      • outerEnvironmentReference: 직전 컨텍스트의 L.E 저장
    • thisBinding
  • 호이스팅
    • environmentRecord의 수집과정을 추상화한 개념
    • 식별자 선언부만을 호이스팅, 할당은 원래 자리
    • 함수 선언문, 함수 표현식에서 차이를 보임
  • 스코프
    • 식별자의 유효범위
    • 어떠한 변수에 접근시 해당 실행 컨텍스트 내의 LE를 보고 없다면 스코프 체이닝을 타고 탐색 (전역변수까지 찾지 못한다면 undefined)
profile
자신을 개발하는 개발자!

0개의 댓글