writed by dikum98

🤔 실행 컨텍스트의 생성과 식별자 검색 과정

  1. 전역 객체 생성
  2. 전역 코드 평가 (pre-populated)
    1. 전역 실행 컨텍스트 생성
    2. 전역 렉시컬 환경 생성 (스코프와 식별자 관리)
      1. 전역 환경 레코드 생성
        1. 객체 환경 레코드 생성 (var)
        2. 선언적 환경 레코드 생성 (let, const)
      2. this 바인딩
      3. 외부 렉시컬 환경에 대한 참조 결정 (상위 스코프 결정)
  3. 전역 코드 실행
  4. 함수 코드 평가 (pre-populated)
    1. 함수 실행 컨텍스트 생성
    2. 함수 렉시컬 환경 생성 (스코프와 식별자 관리)
      1. 함수 환경 레코드 생성 (매개변수, 함수 내에서 선언한 지역변수 (var, let, const), 중첩함수)
      2. this 바인딩
      3. 외부 렉시컬 환경에 대한 참조 결정 (상위 스코프 결정)
  5. 함수 코드 실행
  6. 함수 코드 실행 종료
  7. 전역 코드 실행 종료

🤔 예제 코드

var x = 1;
const y = 2;

function foo (a) {
	var x = 3;
	const y= 4;

	function bar (b) {
		const z = 5;
		console.log(a + b + x + y + z);
	}
	bar(10);
}

foo(20);

🤔 전역 코드에서의 this 바인딩

전역 환경 레코드의 [[GlobalThisValue]] 내부 슬롯에 this가 바인딩된다. 일반적으로 전역 코드에서 this는 전역 객체를 가리키므로 전역 환경 레코드의 [[GlobalThisValue]] 내부 슬롯에는 전역 객체가 바인딩된다. 전역 코드에서 this를 참조하면 전역 환경 레코드의 [[GlobalThisValue]] 내부 슬롯에 바인딩되어 있는 객체가 반환된다.

참고로 전역 환경 레코드를 구성하는 객체 환경 레코드와 선언적 환경 레코드에는 this 바인딩이 없다. this 바인딩은 전역 환경 레코드와 함수 환경 레코드에만 존재한다.

🤔 외부 렉시컬 환경에 대한 참조(Outer Lexical Environment Reference) 결정

외부 렉시컬 환경에 대한 참조는 현재 평가중인 소스코드를 포함하는 외부 소스코드의 렉시컬 환경, 즉 상위 스코프를 가리킨다. 이를 통해 단방향 링크드 리스트인 스코프 체인을 구현한다.

전역 코드를 포함하는 소스코드는 없으므로 전역 렉시컬 환경의 외부 렉시컬 환경에 대한 참조에는 null이 할당된다.

🤔 함수 환경 레코드(Function Environment Record)

함수 환경 레코드는 매개변수, arguments 객체, 함수 내부에서 선언한 지역 변수와 중첩 함수를 등록하고 관리한다.

🤔 함수 코드에서의 this 바인딩

함수 환경 레코드의 [[ThisValue]] 내부 슬롯에 this가 바인딩된다. [[ThisValue]] 내부 슬롯에 바인딩될 객체는 함수 호출 방식에 따라 결정된다.

🤔 외부 렉시컬 환경에 대한 참조(Outer Lexical Environment Reference) 결정

자바스크립트 엔진은 함수 정의를 평가하여 함수 객체를 생성할 때 현재 실행 중인 실행 컨텍스트의 렉시컬 환경, 즉 함수의 상위 스코프를 함수 객체의 내부 슬롯 [[Environment]]에 저장한다. 함수 렉시컬 환경의 외부 렉시컬 환경에 대한 참조에 할당되는 것은 바로 함수의 상위 스코프를 가리키는 함수 객체의 내부 슬롯 [[Environment]]에 저장된 렉시컬 환경의 참조다. 즉, 함수 객체의 내부 슬롯 [[Environment]]가 바로 렉시컬 스코프를 구현하는 메커니즘이다.

함수 객체(함수 정의) 평가 시점은 함수 호출 전, 즉 상위 코드의 평가(전역) 혹은 실행(함수) 단계
함수 코드(함수 몸체) 평가 시점은 함수 호출 후, 즉 현재 함수 코드의 평가 단계

🤔 console.log()의 호출 과정?

console.log 메서드를 호출하기 위해 먼저 식별자인 console을 스코프 체인을 통해 검색한다. 스코프 체인은 현재 실행 중인 실행 컨텍스트의 렉시컬 환경에서 시작하여 외부 렉시컬 환경에 대한 참조로 이어지는 렉시컬 환경의 연속이다.

하지만 console 식별자는 스코프 체인에 등록되어 있지 않고 객체 환경 레코드의 BindingObject를 통해 전역 객체에 바인딩되어 있다. 이는 전역 객체의 프로퍼티가 마치 전역 변수처럼 전역 스코프를 통해 검색 가능해야 한다는 것을 의미한다.

이제 console 식별자에 바인딩된 객체, 즉 console 객체에서 log 메서드를 검색한다. 이때 console 객체의 프로토타입 체인을 통해 메서드를 검색한다. log 메서드는 상속된 프로퍼티가 아니라 console 객체가 직접 소유하는 프로퍼티다.

식별자 → 스코프 체인
메서드 → 프로토타입 체인

🤔 실행 컨텍스트와 블록 레벨 스코프

실행 컨텍스트는 식별자(변수, 함수, 클래스 등의 이름)를 등록하고 관리하는 스코프와 코드 실행 순서 관리를 구현한 내부 메커니즘이다.

var 키워드로 선언한 변수는 오로지 함수의 코드 블록만 지역 스코프로 인정한다. 하지만 let, const 키워드로 선언한 변수는 블록 레벨 스코프를 따른다.

for문 뿐만 아니라 if문도 블록 스코프를 생성하므로, if문의 코드 블록이 실행되면 if문의 코드 블록을 위한 블록 레벨 스코프를 생성해야 한다. 이를 위해 선언적 환경 레코드를 갖는 렉시컬 환경을 새롭게 생성하여 기존의 전역 렉시컬 환경을 '잠시' 교체한다. (실행 컨텍스트를 생성하지는 않는다.)

이때 새롭게 생성된 if문의 코드 블록을 위한 렉시컬 환경의 외부 렉시컬 환경에 대한 참조(상위스코프)는 if문이 실행되기 이전의 전역 렉시컬 환경을 가리킨다.

이는 if문 뿐만 아니라 블록 레벨 스코프를 생성하는 모든 블록문에 적용된다.

for문의 변수 선언문에 let 키워드를 사용한 for문은 코드 블록이 반복해서 실행될 때마다 매번 코드 블록을 위한 새로운 렉시컬 환경을 생성한다. 만약 for문의 코드 블록 내에서 정의된 함수가 있다면 이 함수의 상위 스코프는 for문의 코드 블록이 생성한 렉시컬 환경이다.

이때 함수의 상위 스코프는 for문의 코드 블록이 반복해서 실행될 때마다 식별자(for 문의 변수 선언문 및 for문의 코드 블록 내에서 선언된 지역 변수 등)의 값을 유지해야 한다. 이를 위해 for문의 코드 블록이 반복해서 실행될 때마다 독립적인 렉시컬 환경을 생성하여 식별자의 값을 유지한다.

const arr = [];

for (let i = 0; i < 3; i++) {
  arr.push(function () {
		// 이 함수는 자신이 선언된 시점의 렉시컬 환경(블록 스코프)을 기억한다.
    console.log(i);
  });
}

arr[0](); // 0
arr[1](); // 1
arr[2](); // 2

위 코드에서 각각의 함수는 자신이 선언된 시점의 i 값을 기억하고 있다. 이것은 각각의 함수가 자신만의 독립적인 렉시칼 환경(블록 스코프)을 참조하기 때문이다.

반면에, 만약 var 키워드를 사용하여 같은 코드를 작성한다면 결과가 다르게 나타난다.

const arr = [];

for (var i = 0; i < 3; i++) {
  arr.push(function () {
    console.log(i);
  });
}

arr[0](); // 3
arr[1](); // 3
arr[2](); // 3

이 경우 모든 함수들이 동일한 렉시컬 환경(전역 스코프) 내에서 선언된 변수 i를 참조하므로, 모든 함수들이 마지막으로 할당된 값인 '3'을 출력한다.

profile
Whether you're doing well or not, just keep going👨🏻‍💻🔥

0개의 댓글