JS Study 2주차 ( 실행 컨텍스트 )

jaehan·2023년 4월 1일
0

JavaScript

목록 보기
19/33
post-thumbnail

🖥 실행 컨텍스트

💡 소스코드

ECMAScript 사양은 소스코드를 4가지 타입으로 구분한다.

  • 전역 코드: 전역에 존재하는 소스코드, 전역에 정의된 함수, 클래스 등의 내부코드는 포함되지 않음.
  • 함수 코드: 함수 내부에 존재하는 소스코드, 함수 내부에 중첩된 함수, 클래스 등의 내부 코드는 포함되지 않음.
  • eval 코드: 빌트인 전역 함수인 eval 함수에 인수로 전달되어 실행되는 소스코드.
  • 모듈 코드: 모듈 내부에 존재하는 소스코드, 모듈 내부의 함수, 클래스 등의 내부 코드는 포함되지 않음.

💡 소스코드의 평가와 실행

자바스크립트 엔진은 소스코드를 '소스코드의 평가', '소스코드의 실행' 과정으로 나누어 처리한다

평가

평가 과정에서는 실행 컨텍스트를 생성하고 변수, 함수등의 선언문만 먼저 실행에 변수나 함수 식별자를 키로 실행 컨텍스트가 관리하는 스코프에 등록한다.

실행

선언문을 제외한 소스코드가 순차적으로 실행된다. -> 런타임

변수나 함수의 참조를 실행 컨텍스트가 관리하는 스코프에서 검색해서 사용한다.

또한 값이 바뀌면 실행 컨텍스트가 관리하는 스코프에 등록한다.

예시

x = 1;

let x;

위 코드의 과정을 살펴보면

  1. 평가과정이 먼저 시작되어 x가 실행컨텍스트가 관리하는 스코프에 등록된다
    -> 키: x 값: undefined

  2. 평가과정이 끝나 실행과정으로 들어가면 x = 1;코드가 실행되어 x가 스코프에 있는지 확인한 후에 있기 때문에 x를 키값으로 접근해 1을 재할당한다.
    -> 키: x 값: 1

💡 실행컨텍스트 스택

위처럼 자바스크립트 엔진이 소스코드를 처리할때 평가실행으로 나눈다고 했다
근데 아래처럼 함수가 중첩이 되면 함수 내에서도 평가실행이 일어나야 한다.

이게 점점 깊이가 깊어지면 너무 복잡해지기 때문에 실행컨텍스트 스택을 이용해서 관리한다.

const x = 1;
function foo() {
  const y - 2;
  
  function bar() {
    const z = 3;
    console.log(x + y + z);
  }
  bar();
}

foo();

위 코드의 처리 과정을 살펴보면

  1. 전역 코드의 평가와 실행:

    • 평가: 전역 실행 컨텍스트를 생성 -> 실행 컨텍스트 스택에 push -> x, foo 등록
    • 실행: foo 호출됨
  2. foo 함수 코드의 평가와 실행

    • foo 함수가 호출되었기 때문에 위의 전역 실행은 멈추고 아까 위에서 4가지 중에 함수 코드의 평가와 실행이 시작된다.
    • 평가: foo 함수 실행 컨텍스트 생성 -> 실행 컨텍스트 스택에 push -> y, bar 등록
    • 실행: bar 호출됨
  3. bar 함수 코드의 평가와 실행

    • foo 함수의 실행을 멈추고 bar 함수의 평가와 실행이 시작됨
    • 평가: bar 함수 실행 컨텍스트 생성 -> 실행 컨텍스트 스택에 push -> z 등록
    • 실행: console.log 메서드 호출후 종료
  4. foo 함수 코드로 복귀

    • bar 함수 실행 컨텍스트 pop
    • 실행 이어감: 더이상 없으니 종료
  5. 전역 코드로 복귀

    • foo 함수 실행 컨텍스트 pop
    • 실행 이어감: 더이상 없으니 종료

전역 실행 컨텍스트도 pop -> 이제 스택에는 아무것도 없음

이처럼 실행 컨텍스트 스택을 이용해 코드의 실행 순서를 관리한다.

최상위의 실행 컨텍스트는 실행중인 컨텍스트라 부른다

💡 렉시컬 환경

렉시컬 환경은 식별자와 식별자에 바인딩된 값, 그리고 상위 스코프에 대한 참조를 기록하는 자료구조.
실행 컨텍스트를 구성하는 컴포넌트이다.

한마디로 렉시컬 환경은 객체형태의 스코프를 생성해서 식별자를 키로 등록하고 그 키에대해 값을 관리하는 저장소이다.

렉시컬 환경은 두개의 컴포넌트로 구성된다

  • 환경 레코드 Environment Record: 값을 저장하는 저장소
  • 외부 렉시컬 환경에 대한 참조 Outer Lexical Environment Reference: 상위 스코프를 가리키는 포인터라고 생각하면 될것 같다.

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

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

위 코드를 기준으로 실행 컨텍스트가 어떻게 생성되는지 살펴보면

1. 전역 객체 생성

전역객체는 전역 코드가 평가되기 이전에 생성된다. 그냥 우리가 선언하지 않는 내부 빌트인 함수 같은거라고 생각하면 된다.

2. 전역 코드 평가

전역 코드 평가의 실행 결과는 우선 아래와 같다

하나씩 살펴보면

2-1. 전역 실행 컨텍스트 생성

비어있는 전역 실행 컨텍스트를 생성해서 실행컨텍스트 스택에 푸시한다.

2-2. 전역 렉시컬 환경 생성

전역 렉시컬 황경을 생성하고 전역 실행 컨텍스트와 바인딩 한다.

📌 전역 환경 레코드 생성

전역 환경 레코드는 앞에서 언급한것 처럼 값을 저장하는 저장소이다.

호이스팅을 생각해 보면 var는 선언과 동시에 undefined로 할당 되지만 let과 const는 그렇지 않았다.

따라서 전역 환경 레코드에서도 둘을 따로 객체 환경 레코드와 선언적 환경 레코드로 나눠서 관리한다.

2-3. 객체 환경 레코드

객체 환경 레코드는 var 키워드로 선언한 전역변수와 함수 선언문으로 정의한 전역함수, 빌트인 전역 프로퍼티와 빌트인 전역함수, 표준 빌트인 객체를 관리한다. -> ❗️ 이 말은 맨 처음 만들어진 전역 객체와 같이 관리된다는 말이다

객체 환경 레코드는 BindingObject라고 부르는 객체와 연결된다.

2-4.

전역 코드 평가 과정에서 var 키워드로 선언한 전역 변수와 함수 선언문으로 정의된 전역 함수는 전역 환경 레코드의 객체 환경 레코드에 연결된 BindingObject를 통해 전역 객체의 프로퍼티와 메서드가 된다.

📌 그래서 그림 맨 오른쪽으 window를 보면 x가 var로 선언되었기 때문에 undefined로 초기화 되어있고 foo가 함수로 할당되어 있다.
❗️ 함수는 함수 객체를 즉시 할당받는다

2-5. 선언적 환경 레코드 생성

전역변수 y는 const로 선언되었기 때문에 선언적 환경 레코드에 y라는 키값으로 들어가고 값은 참조할 수 없다는 뜻인 <uninitialized>로 할당된다

2-6. this 바인딩

this는 전역객체를 가리키기 때문에 전역객체와 바인딩 된다.

2-7. 외부 렉시컬 환경에 대한 참조

현재는 전역 코드이기 때문에 참조를 하지 않으므로 null이 들어간다

함수 코드나 더 아래의 코드는 외부 소스코드의 렉시컬 환경, 즉 상위 스코프를 가리킬 것이다.

3. 전역 코드 실행

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

2번까지 x, y, foo를 선언해놨으니 이제 전역 코드를 실행시킨다.

x에 1이 할당되고 y에 2가 할당된다.

그다음으로 foo함수가 호출된다.

📌 이렇게 코드를 실행할때 변수나 함수를 사용한다면 선언되었는지 확인해야 한다. 또한 식별자는 스코프가 다르면 같은 이름을 가질 수 있기 때문에 식별자 결정을 통해 스코프의 위치를 결정해야한다.
📌 식별자 결정은 현재 실행중인 컨텍스트부터 시작해서 외부 렉시컬 환경에 대한 참조가 가리키는 상위 스코프로 이동한다. 이떄 전역 렉시컬 환경이 마지막이다.

4. foo 함수 코드 평가

foo 함수가 호출되었기 때문에 아래의 코드가 평가된다고 생각하면 된다.

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

결과는 위와 같다

순서대로 살펴보면

4-1. 함수 실행 컨텍스트 생성

전역 실행 컨텍스트는 생성하자 마자 푸시했지만 함수 실행 컨텍스트는 함수 렉시컬 환경이 완성된 다음 푸시 된다. 푸시되고 나면 이 컨텍스트가 실행중인 컨텍스트가 된다

4-2. 함수 렉시컬 환경 생성

foo 함수 렉시컬 환경을 생성하고 함수 실행컨텍스트에 바인딩한다.

4-3. 함수 환경 레코드 생성

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

그렇기 때문에 매개변수 a, arguments 객체, 지역변수 x, y 중첩 함수 bar가 등록되었다.

4-4. this 바인딩

foo 함수는 일반 함수이기 때문에 this는 전역 객체를 가리킨다.
foo 함수 내부에서 this를 참조하면 함수 환경 레코드의 [[ThisValue]] 내부 슬록에 바인딩 되어있는 객체가 반환됨

❗️ 현재 foo함수의 this는 전역 객체를 가리키기 때문에 아래 코드의 결과에 x 1이 출력된다.

4-5. 외부 렉시컬 환경에 대한 참조 결정

결정은 foo함수 정의가 평가된 시점의 실행컨텍스트의 렉시컬 환경의 참조가 할당된다.

현재 foo가 전역코드의 전역 객체에 정의되어 있기 때문에 전역 렉시켤 환경의 참조가 할당된다.

5. foo 함수 코드 실행

코드가 실행되면

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

a에 20, x에 3, y에 4가 할당되고 bar 함수가 호출된다.

📌 여기서 전역에도 x, y가 있지만 함수 내부의 x, y에 할당되는 이유는
식별자 결정 과정에서는 현재 실행중인 컨텍스트 내부에서 먼저 찾는다.
현재 실행중인 컨텍스트는 foo 실행 컨텍스트이기 때문에 여기서 먼저 찾는데 x, y 모두 선언되어 있기 때문에 foo 실행 컨텍스트에서 선언된 곳에 값을 할당한다.

6. bar 함수 코드 평가

위에서 bar 함수가 호출되었기 때문에 bar 함수 코드평가가 시작된다.

그림은 아까 foo 함수 코드 평가의 그림과 비슷하기 때문에 생략한다.

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

순서만 정리해 보면 아래와 같다

  1. 함수 실행 컨텍스트 생성
  2. 함수 렉시컬 환경 생성
  3. 함수 환경 레코드 생성
  4. this 바인딩
  5. 외부 렉시컬 환경에 대한 참조 결정
    • bar는 foo 렉시컬 환경에 정의되어 있기 때문에 foo 렉시컬 환경을 참조함

7. bar 함수 코드 실행

b에 10, z에 5가 할당되고 console.log(a + b + x + y + z) 가 실행된다

1. console 식별자 검색

현재 bar 실행 컨텍스트 부터 전역까지 스코프 체인을 통해 검색한다.

console은 전역 객체 환경 레코드의 BindingObject를 통해 전역 객체에서 찾을 수 있다.

2. log 메서드 검색

log메서드는 console 객체가 소유하는 프로퍼티이기 때문에 console 객체에서 log를 검색한다.

3. a + b + x + y + z

a는 foo, b는 bar, x, y는 foo, z는 bar 렉시컬 환경에서 검색되어 값을 가져온다

4. console.log 메서드 호출

위의 값을 console.log 메서드에 전달하여 호출한다

8. bar 함수 코드 실행 종료

더 할게 없기 때문에 코드의 실행이 종료되고 bar 함수 실행 컨텍스트가 pop되고 foo 함수 실행 컨텍스트가 실행중인 실행 컨텍스트가 된다.

📌 foo 렉시컬 환경의 bar 함수가 즉시 소멸되지는 않고 누군가에게 참조되지 않을때 가비지 컬렉터가 소멸시킨다

9. foo 함수 코드 실행 종료

foo 도 할게 없기 때문에 실행이 종료 되고 pop되어 전역 실행 컨텍스트가 실행중인 실행 컨텍스트가 된다.

10. 전역 코드 실행 종료

전역 코드의 실행도 종료되고 pop되어 실행 컨텍스트 스택에는 아무것도 남지 않게 된다.

💡 블록 레벨 스코프

이전에 배웠듯이 var는 함수 레벨 스코프를 따르지만 let, const는 블록 레벨 스코프이다.

앞에서는 함수를 예로 했었는데 아래 코드를 예로 들면

let x = 1;

if (true) {
  let x = 10;
  console.log(x); // 10
}
console.log(x); // 1

함수는 실행 컨텍스트 스택에 push되어 동작했다면

블록문은 블록 내의 렉시컬 환경을 생성하고 현재 실행중인 컨텍스트가 현재의 렉시컬 환경을 블록 렉시컬 환경으로 교체해서 평가및 실행을 진행하고 기존 렉시컬 환경으로 복귀한다.

📖 요약

실행 컨텍스트 내용은 진짜 중요하다고 생각이 든다.

지금까지 js 코딩 해오면서 얼추 동작은 알았지만 이렇게 까지 자세하게는 몰랐어서 정말 많은걸 배운것 같다.

중요하기 때문에 한번더 요약해보자면

  1. 자바스크립트 엔진은 소스코드를 평가와 실행으로 나누어서 처리한다.
  2. 실행 컨텍스트 스택은 실행 컨텍스트의 동작을 관리하는 스택이다
  3. 렉시컬 환경은 객체형태의 스코프를 생성해서 식별자를 키로 등록하고 그 키에대해 값을 관리하는 저장소이다
  4. 코드의 동작은
    전역 객체 생성 -> 전역 코드 평가 -> 전역 코드 실행 -> 중간에 함수가 있다면 함수 코드 평가 -> 함수 평가 실행 ... 이 반복되어 결국엔 전역 코드 실행으로 돌아와서 끝난다.
  5. 전역 코드 평가의 순서는
1. 전역 실행 컨텍스트 생성
2. 전역 렉시컬 환경 생성
	2.1. 전역 환경 레코드 생성
		2.1.1. 객체 환경 레코드 생성
		2.1.2. 선언적 환경 레코드 생성
	2.2. this 바인딩
	2.3. 외부 렉시컬 환경에 대한 참조 결정
  1. 함수 코드 평가의 순서는
1. 함수 실행 컨텍스트 생성
2. 함수 렉시컬 환경 생성
	2.1. 함수 환경 레코드 생성
	2.2. this 바인딩
	2.3. 외부 렉시컬 환경에 대한 참조 결정

실행 컨텍스트는 자주 되새겨야 할것 같다

참고: https://www.inflearn.com/course/%EB%AA%A8%EB%8D%98-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%EB%94%A5%EB%8B%A4%EC%9D%B4%EB%B8%8C

0개의 댓글