코어 자바스크립트 - 실행 컨텍스트

Hwang Tae Young·2023년 2월 22일
0

✅ 실행 컨텍스트란?

  • 실행할 코드에 제공할 환경 정보들을 모아놓은 객체로 자바스크립트의 동적 언어로서의 성격을 가장 잘 파악할 수 있는 개념이다.
  • 실행 컨텍스트가 활성화되는 시점에 선언된 변수를 위로 끌어올리고(호이스팅) 외부 환경 정보를 구성하고, this 값을 설정하는 등 동작을 수행한다.

1. 동작 방식

  1. 동일한 환경에 있는 코드들을 실행할 때 필요한 환경 정보들을 모아 컨텍스트를 구성

  2. 구성된 컨텍스트들을 차례대로 콜 스택에 쌓아 올렸다가

  3. 가장 위에 쌓여있는 컨텍스트와 관련 있는 코드들을 실행하는 식으로 전체 코드의 환경과 순서를 보장합니다.

    스택(Stack)이란?
    Last In First Out 라고도 하며, 나중에 쌓인 데이터부터 먼저 처리되는 방식을 말합니다.
    실행 순서 A -> B -> C -> D -> E

    이미지 출처 : 네이버 블로그 : 자바로배우는 알고리즘 기초

2. 구성 방법

  • 자동으로 생성되는 전역 공간입니다.
  • eval() 함수 -> 추천하지 않는 방법입니다.
  • 함수를 실행하는 방법으로 우리가 가장 흔히 사용하는 방법입니다.
  • { }으로 둘러쌓인 코드 내부 ES6부터 지원합니다.

3. 실행 콘텍스트와 콜 스택

// ------------------- (1)
var a = 1;
function outer() {
  function inner() {
    console.log(a);
    var a = 3;
    // --------------- (2)
  }
  inner(); // -------- (3)
  console.log(a);
  // ----------------- (4)
}
outer(); // ---------- (5)
console.log(a);
// ------------------- (6)
  • 실행 순서 : (1) → (5) → (3) → (2) → (4) → (6)
    1. 자바스크립트 코드가 실행하는 순간 전역 컨텍스트가 콜 스택에 쌓임
      전역 컨텍스트라는 개념은 일반적인 실행 컨텍스트와 특별히 다를것이 없고, 자바스크립트 파일이 열리는 순간 활성화 됨
    2. 차근차근 코드를 실행하다가 outer 함수를 호출하면서 outer에 대한 환경 정보를 수집해서 outer 실행 컨텍스트를 생성한 뒤 콜 스택에 담습니다.
      콜 스택 맨 위에 outer 실행 컨텍스트가 놓인 상태가 됐으므로 전역 컨텍스트와 관련된 코드의 실행을 일시 중단하고 outer 실행 컨텍스트와 관련된 코드, 즉 outer 함수의 내부 코드들을 순차적으로 실행합니다.
    3. 마찬가지로 outer함수의 내부에 있는 inner 함수가 호출되고 실행 컨텍스트가 콜 스택 가장 위에 담기면, 아까와 마찬가지로 outer 실행 컨텍스트와 관련된 코드의 실행을 중단하고 inner 함수 내부의 코드를 순서대로 진행합니다.
    4. inner 함수 내부에서 변수 a에 3을 할당하고 나면 inner 함수가 종료되면서 inner 실행 컨텍스트가 제거됩니다.
    5. 그러면 그 아래 쌓인, outer 컨텍스트가 콜 스택 맨 위로 올라오고 a의 값을 출력하고 함수의 실행이 종료되고 outer 실행 컨텍스트가 제거됩니다.
    6. 마지막으로 전역 컨텍스트에서 a의 값을 출력하고 나면 더이상 실행할 코드가 없기 때문에, 전역 컨텍스트도 제거되고 콜 스택에는 아무것도 남지 않은 상태로 종료됩니다.

실행 컨텍스트가 콜 스택의 맨 위에 쌓이는 순간이 곧 현재 실행할 코드에 관여하게 되는 시점이고
실행 컨텍스트가 활성화될 때, 자바스크립트 엔진은 해당 컨텍스트에 필요한 환경 정보들을 수집해서 실행 컨텍스트 객체에 저장합니다.
저장되는 환경 정보는 다음과 같습니다.

실행 콘텍스트의 환경

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

스냅샷이란?
마치 사진 찍듯이 특정 시점에 스토리지의 파일 시스템을 포착해 보관하는 기술

✅ 환경 정보

1. VariableEnvironment

  • 담기는 내용은 LexicalEnvironment와 같지만 최초 실행 시의 스냅샷을 유지한다는 점이 차이점입니다.
  • 실행 컨텍스트를 생성 할 때 VariableEnvironment에 정보를 먼저 담은 다음, 이를 그대로 복사해서 LexicalEnvironment를 만들고, 이후에는 LexicalEnvironment를 주로 활용하게 됩니다.
  • 내부 구성요소는 environmentRecord와 outer-EnvironmentReference 입니다.
  • 초기화 과정 중에는 사실상 완전히 동일하고 코드 진행에 따라 서로 달라지게 됩니다.

2. LexicalEnvironment

  • "현재 컨텍스트 내부에는 a, b, c와 같은 식별자들이 있고 그 외부 정보는 D를 참조하도록 구성돼있다"라는 식으로 이해하는 것이 좋을 수도 있다.
  • 내부 구성요소는 environmentRecord와 outer-EnvironmentReference 입니다.

✅ environmentRecord와 호이스팅

  • environmentRecord

    • 현재 컨텍스트와 관련된 코드의 식별자 정보들이 저장됩니다.
      • 컨텍스트를 구성하는 함수에 지정된 매개변수 식별자
      • 선언한 함수가 있을 경우 그 함수 자체
      • var let const 등 선언 된 변수의 식별자
    • 컨텍스트 내부 전체를 처음부터 끝까지 쭉 훑어나가며 순서대로 수집합니다.
    • 참고
      • 전역 컨텍스트는 변수 객체를 생성하는 대신 자바스크립트 구동 환경이 별도로 제공하는 객체, 전역 객체를 활용함
      • 전역 객체에는 브라우저의 window, Node.js의 global객체 등이 있습니다.
      • 이들은 내장(native) 객체가 아닌 호스트(host) 객체로 분류합니다.
  • 호이스팅의 등장과 개념

    • 변수 정보를 수집하는 과정을 모두 마친다면, 실행 컨텍스트가 관여할 코드들은 실행되기 전의 상태임에도 자바스크립트 엔진은 이미 해당 환경에 속한 코드의 변수명들을 모두 알고 있게됩니다.
    • "자바스크립트 엔진은 식별자들을 최상단으로 끌어올려놓은 다음 실제 코드를 실행한다"라고 생각하더라도 코드를 해석하는데는 문제가 없을 것입니다.
    • 여기서 호이스팅이라는 개념이 등장하게 됩니다. 끌어올리다라는 뜻으로 자바스크립트 엔진이 실제로 끌어올리지는 않지만, 편의상 끌어올린 것으로 간주하는 것
  • 호이스팅의 규칙

    • environmentRecord는 현재 실행 될 컨텍스트 대상 코드 내에 어떤 식별자들이 있는지에만 관심이 있지,
      각 식별자들이 어떤 값이 할당 될지에는 관심이 없습니다.
    • 그렇기 때문에, 변수를 호이스팅 할때도 변수명만 끌어올리고 할당 과정은 원래 자리에 그대로 남겨둡니다.

1. 변수의 호이스팅

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

a(1)
  • 일반적으로 생각해 보았을때 1 ~ 3의 값을 예상해보자면

    1. 매개 변수로 전달된 1
    2. 지역변수로 x가 선언되었지만, 할당되지 않았기 때문에 undefined
    3. 지역변수로 x에 할당된 2
  • 이렇게 생각이 들겠지만 실제로는 1, 1, 2가 나와버렸습니다.

🤔 왜 그럴까?

  • a 함수를 실행하는 순간, a 함수의 실행 컨텍스트가 생성된다. 이때 변수명과 함수 선언의 정보를 위로 끌어올린다.
  • 그래서 사실상 아래와 같이 동작한다고 생각하면 될 것 같다.
function a (x) { 
	var x;  // 수집 대상 1의 변수 선언 부분
	var x;  // 수집 대상 2의 변수 선언 부분
	var x;  // 수집 대상 3의 변수 선언 부분
	
	x = 1;  // 수집 대상 1의 할당 부분
	console.log(x); //------------ (1)
	console.log(x); //------------ (2)
	x = 2; // 수집 대상 2의 할당 부분
	console.log(x); //------------ (3)
}

a(1)

그리고 이 코드는 아래의 과정을 거쳐 동작한다.

  1. 2번째 줄: 변수 x 선언. 메모리 저장공간을 미리 확보하고, 확보한 공간의 주솟값을 변수 x에 연결합니다.
  2. 3번째, 4번째 줄: 다시 변수 x를 선언하나, 이미 있으므로 무시합니다.
  3. 6번째 줄: x에 1을 할당. 우선 숫자 1을 별도의 메모리에 담고, x가 1의 주소값을 입력합니다.
  4. 7번째, 8번째 줄: 각 x출력. 모두 1이 출력됩니다.
  5. 9번째 줄: x에 2를 할당. 기존의 주소값을 2의 주소값으로 바꿉니다.
  6. 10번째 줄: x를 출력. 2가 출력됩니다.
  7. 함수 내부의 모든 코드가 실행됐으므로 실행 컨텍스트가 콜 스택에서 제거됩니다.

💡 물론 변수 선언할 때, var를 사용해서 이러한 문제가 생기는 것이고 let & const는 오류를 내뿜어준다,,!

2. 함수 선언의 호이스팅

function a() {
  console.log(b); // (1)
  var b = 'bbb'; // 수집 대상 1(변수 선언)
  console.log(b); // (2)
  function b() {} // 수집 대상 2(함수 선언)
  console.log(b); // (3)
}
a();
  • 다시 1 ~ 3의 값을 예상해보자면

    1. 변수, 함수의 선언부가 호이스팅 되어 b가 선언은 되어 있으나 값이 없으니 undefined
    2. 바로 위에서 b에 'bbb'를 할당 받았으니 bbb
    3. 바로 위에 b를 함수로 선언 했으니 function b() {}
  • 이렇게 생각이 들겠지만 결과는 function b() {}, bbb, bbb가 나와버렸습니다.

function a() {
  var b; // 수집 대상 1. 변수는 선언부만 끌어올립니다.
  function b() {} // 수집 대상 2. 함수 선언은 전체를 끌어올립니다.
  // var = b function b () {} // ( * )
  
  console.log(b); // (1)
  b = 'bbb'; // 변수의 할당부는 원래 자리에 남겨둡니다.
  console.log(b); // (2)
  console.log(b); // (3)
}
a();
  • 변수는 선언부와 할당부를 나눠 선언부만 끌어올리지만, 함수는 본인 그 자체 함수를 끌어 올립니다
  • 호이스팅이 끝난 상태에서의 함수 선언문은 (*)과 같이 함수명으로 선언한 변수에 함수를 할당한 것처럼 여길 수 있습니다.
  • 또한 그렇게 해도 결과는 같게 나옵니다.

3. 함수 선언문과 함수 표현식

  • 함수 선언문
    • 함수 정의부만 존재하고 별도의 할당 명령이 없는 것입니다.
  • 함수 표현식
    • 정의한 함수를 별도의 변수에 할당하는 것을 말합니다.
    • 함수명을 정의한 표현식은 기명 함수 표현식이라 하고, 정의하지 않은 것은 익명 함수 표현식이라고 부르며 일반적으로 함수 표현식이라고 함은 익명 함수 표현식을 말합니다.
function a() {
  /* ... */
} // 함수 선언문. 함수명 a가 곧 변수명.
a(); // 실행 OK.

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

var c = function d() {
  /* ... */
}; // 기명 함수 표현식. 변수명은 c, 함수명은 d.
c(); // 실행 OK.
d(); // 실행 불가능 에러!
  • 함수 선언문의 위험성
    • 함수 선언문은 중복되서 선언되더라도 오류가 나오지 않고, 나중에 선언된 함수가 먼저 선언된 함수를 덮어써 그 함수만 사용하게 된다.
a();

function a() {
  console.log("먼저 선언 된 함수");
}

a();

function a() {
  console.log("나중에 선언 된 함수");
}

a();
---결과
나중에 선언 된 함수
나중에 선언 된 함수
나중에 선언 된 함수

✅ 스코프, 스코프 체인, outerEnvironmentRecord

  • 스코프 (Scope) : 식별자에 대한 유효 범위
    • ES5까지는 전역 공간을 제외한다면 오직 함수에 의해서만 스코프가 생성되었다.
  • 스코프 체인 (Scope chain) : 식별자에 대한 유효 범위를 안에서 부터 바깥으로 차례로 검색해 나가는 것
    • 스코프 체인을 가능하게 하는 것이 outerEnvironmentReference

✅ 스코프 체인

  • outerEnvironmentReference는 현재 호출된 함수가 선언될 당시의 LexicalEnvironment을 참조하게 됩니다.
  • 현재 스코프에 선언 된 식별자가 없으면 상위 스코프로 이동하여 검색하는 연결리스트의 형식을 띄게 됩니다.
  • 여러 스코프에서 동일한 식별자를 선언한 경우에는 무조건 스코프 체인 상에서 가장 먼저 발견된 식별자에만 접근 가능
  • 변수 은닉화 (Variable shadowing) : 상위 스코프에 선언되어 있지만, 현재 스코프에 이미 선언된 경우에는 값이 할당되어 있지 않다고 하더라도 현재 스코프의 변수를 우선으로 함
var a = 1;
var outer = function() {
  var inner = function() {
    console.log(a); // 1
    var a = 3;
  };
  inner();
  console.log(a); // - 2
}
outer();
console.log(a); // --- 3

  • 동작방식
    1. inner 함수 컨텍스트에는 a 식별자가 존재하지만 아직 값이 할당되지 않았기 때문에 undefined 을 출력
    2. outer 함수 컨텍스트에는 a 식별자가 존재하지 않으므로, outerEnvironmentReference를 따라가서 전역 컨텍스트의 a에 들어 있는 1을 출력
    3. 전역 컨텍스트에는 a 식별자가 존재하고 값이 존재하므로 1 을 출력

✅ 전역변수와 지역변수

  • 전역변수 : 전역 공간에서 선언한 변수
    • 전역에 변수를 선언하면 이 변수는 어디서든지 참조가 가능하다.
    • var 키워드로 선언한 전역 변수는 전역 객체 window의 프로퍼티가 된다.
    • 전역 변수의 사용은 변수 이름이 중복될 수 있고, 의도치 않은 재할당에 의한 상태 변화로 코드를 예측하기 어렵게 만드므로 사용을 지양해야한다.
  • 지역변수 : 지역 공간에서 선언한 변수

✅ this

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

참고 : modolee velog : 코어 자바스크립트 - 02 실행 컨텍스트
참고 : 코어 자바스크립트

스코프체인에 대한 부분이 이해가 될듯 말듯하다,,,, 내가 설명하라고 하면 못하니 다시 공부 해봐야겠다,,!

profile
더 나은 개발자가 되기 위해...☆

0개의 댓글