[JavaScript] 실행 컨텍스트 (Execution Context)


1. 실행 컨텍스트(Execution Context)

(1) 실행 컨텍스트는?

  • 실행 할 코드에 제공할 환경 정보들을 모아 놓은 객체

(2) 스택과 큐

  • Stack : a,b,c,d를 저장하고 꺼낼때는 d,c,b,a 순서로 꺼내는 방법 (후입선출)
  • Queue: a,b,c,d를 저장하고 꺼낼때 a,b,c,d 순서로 꺼내는 방법 (선입선출)

(3) 실행 컨텍스트 구성 방법

  • 전역공간
  • eval() 함수
  • 함수
// --------------------------------  전역 컨텍스트

var a = 1;

function outer() {
  function inner() {
    console.log(a); // undefined
    var a = 3;
  }
  inner(); //--------------------- inner 실행 컨텍스트
  console.log(a); // 1
}

outer(); //------------------------ outer 실행 컨텍스트
console.log(a); // 1

(1) 콜 스택 쌓이는 순서

콜 스택 쌓이는 순서

  • 코드를 실행하면 전역 컨텍스트가 콜 스택에 먼저 담김
  • outer()함수를 호출하면 outer()에 대한 환경 정보를 수집해서 outer() 실행 컨텍스트를 생성하고 콜 스택에 담는다
  • outer()에 담긴 inner() 함수의 실행 컨텍스트를 콜 스택에 담는다
  • inner() 함수가 콜 스택 가장 위로 쌓이면 a 변수에 값 3을 할당하고
  • inner() 함수를 콜 스택에서 제거
  • inner() 함수 종료
  • inner() 함수 다음 줄 console.log를 출력
  • outer() 함수를 콜 스택에서 제거
  • outer() 함수가 종료
  • outer() 함수 다음 줄 console.log를 출력
  • 전역 컨텍스트는 실행할 코드가 남아 있지 않아서 다시 제거된다

스택 구조는 실행 컨텍스트가 콜 스택 맨 위에 쌓이는 순간 현재 실행할 코드에 관여하게 되는 시점이다

여기서 실행 컨텍스트의 수집 정보

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

2. Variable Environment

  • 실행 컨텍스트 생성 중 생성 단계의 정보
  • 함수 내의 코드 실행 직전에 생성
  • Environment Record는 컨텍스트 내부를 처음부터 끝까지 순서대로 식별자 정보(이름,함수선언,변수명 등)를 수집하여 작성됌 ( 식별자의 바인딩 정보가 담겨있지만 , 어떤 값이 할당되었는지는 관심이 없다)
  • 따라서 변수 선언만 끌어올리고 원래 자리에 남겨두는 호이스팅이 발생함 (할당은 Lexical Environment의 Environment Record에만 갱신됌

3. Lexical Environment

  • 이미 만들어진 Varibale Environment를 복사
  • 코드를 실행하면서 변수에 값이 할당되거나 변경되면 Lexical Environment에만 업데이트시킴

(1) Environment Record와 Hoisting

  • Environment Record는 컨텍스트 내부를 처음부터 끝까지 순서대로 식별자 정보를 수집하였으니 코드가 실행 전임에도 불구하고 자바스크립트 엔진은 이미 해당 환경에 속한 변수명을 다 아는것
  • 엔진의 실제 동작 방식 대신에 ‘자바스크립트 엔진은 식별자들을 최상단으로 끌어올리고 나서 실제 코드를 실행한다’라고 생각해도 문제가 없을 것 ⇒ 실제로 끌어올리지는 않지만 편의상 끌어올린 것 (호이스팅의 개념)

(1) 코드로 이해해보는 Hoisting 규칙

  • 예시 코드
  • 호이스팅이 안된다고 가정하면 각각의 값은 이렇게 찍힐 것이다.
function a(x) {
  console.log(x); // 1
  var x;
  console.log(x); // undefined
  var x = 2;
  console.log(x); // 2
}

a(1);

실제 구동되는 결과는 ?

  • Environment Record는 식별자에만 관심있지 어떤 값이 할당될건지는 관심없다
  • 실행 전 식별자 정보를 모두 수집했으니 1,1,2가 나온다
function a(x) {
  // 수집 대상 1 (매개변수)
  console.log(x); // 1
  var x; // 수집 대상 2 (변수 선언)
  console.log(x); // 1
  var x = 2; // 수집 대상 2 (변수 선언)
  console.log(x); // 2
}

a(1);
  • 그래서 매개변수를 변수 선언/할당과 같다고 간주해서 변환하면?
  • 위 코드는 아래 코드와 결과값이 같다
function a() {
  // 수집 대상 1 (매개변수)
  var x = 1;
  console.log(x); // 1
  var x; // 수집 대상 2 (변수 선언)
  console.log(x); // 1
  var x = 2; // 수집 대상 2 (변수 선언)
  console.log(x); // 2
}

a();
  • Environment Record에 맞춰 여기서 호이스팅 처리를해서 변수명만 끌어 올리고 할당은 남겨두자면
function a() {
  var x; // 수집 대상 (1) (변수 선언)
  var x; // 수집 대상 (2) (변수 선언)
  var x; // 수집 대상 (3) (변수 선언)

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

a();

코드 실행 과정

  • 수집 대상 (1)의 변수 x를 선언 ⇒ 저장 공간 메모리 확보 , 주솟값을 변수 x에 연결함
  • 수집 대상 (2),(3)은 다시 변수 x를 선언 ⇒ 근데 이미 선언된 x가 있으니 무시한다
  • 할당 부분1에서 x1을 할당 , 숫자 1을 메모리에 담고 x와 연결된 메모리 공간에 숫자 1을 가리키는 데이터 주솟값을 입력한다
  • 첫번째 할당을 마친 console.log(x) 출력 두개 모두 1이 출력된다
  • 할당 부분2에서 x2를 할당 , 숫자 2를 별도의 메모리에 담고 x에 연결된 메모리 주소로 이동
  • 기존에 숫자 1을 가리키는 주솟값을 숫자2로 가리치는 주솟값으로 대치한다
  • 두번째 할당을 마친 console.log(x) 출력엔 2가 출력된다
  • 실행 컨텍스트가 콜 스택에서 제거된다

(2) 함수 선언의 Hoisting 규칙

  • 여기서 Hoisting을 생각 안하고 결과값을 생각하면 이런 식으로 나올것이다
  • (1)에선 console.log에선 값을 할당 안했으니 당연히 undefined
  • (2)에선 ‘bbb’를 할당했으니 ‘bbb’
  • (3)에선 function b
function a() {
  console.log(b); // (1) 'undefined'
  var b = "bbb";
  console.log(b); // (2) 'bbb'
  function b() {}
  console.log(b); // (3) function
}

a();
  • 그치만 결과는 다음과 같다
  • 여기서 변수는 선언부 , 할당부를 나누어서 선언부만 끌어올리지만 함수 선언은 함수 전체를 끌어올린다 따라서 우선적으로 변수 var b = ‘bbb’가 변수 선언부 var b;로 올라가고 함수 선언 function b(){}var b = function b(){}가 된다
[Function: b]
bbb
bbb

[Done] exited with code=0 in 0.05 seconds

위 코드를 이해하고 Hoisting을 생각해서 코드 동작을 변환하면

function a() {
  var b; //  수집 대상 (1)
  var b = function b() {}; //  수집 대상 (2)
  console.log(b); // (1)
  var b = "bbb"; // 데이터 할당(1)
  console.log(b); // (2)
  console.log(b); // (3)
}

a();
  • 첫번째로 수집 대상에서 var b;를 수집한다, 메모리는 저장 공간 확보 후 주솟값을 변수 b에 연결시킨다
  • 이제 선언된 변수 bfunction b를 메모리에 할당한다 (변수 b는 함수를 가리킴)
  • (1)에서는 function b를 할당했으니 [Function: b]를 출력한다
  • 다시 변수 b‘bbb’를 할당한다 기존에 함수가 저장된 주솟값을 ‘bbb’가 담긴 주솟값으로 덮어쓴다
  • b는 선언된 변수에 ‘bbb’를 가리킨다
  • (2) , (3)은 모두 ‘bbb’를 출력한다
  • 실행 컨텍스트가 콜 스택에서 제거된다

(3) 함수 선언문과 함수 표현식

  • 함수 선언문 : function 정의부만 존재하고 별도의 할당 명령이 없는것
  • 함수 표현식 : function을 별도의 변수에 할당하는 것

함수 표현식에는 두가지 종류가 있는데 일반적으론 익명 함수 표현식을 말함

  • 기명함수 : 함수명을 정의한 함수 표현식
  • 익명함수 : 함수명을 정의 안한 함수 표현식
// 함수 선언문
function a() {}
console.log(a); //실행 ok
// 함수 표현식 (익명 함수)
var b = function () {};
b(); //실행 ok
// 함수 표현식 (기명 함수)
var c = function d() {};
c(); //실행 ok
d(); //에러!

여기서 기명함수는 외부에서 함수명으로 함수를 호출할 수 없다.

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

  • 원본코드
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;
};
  • 이 코드를 호이스팅 처리 동작 코드로 만들면
// (1) 함수 선언문은 전체 호이스팅 처리
function sum(a, b) {
  return a + b;
}

// (2) 변수 선언부 가져옴

var multiply;

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

multiply = function (a, b) {
  return a * b;
};
  • 첫번째로 함수 선언문을 최상단으로 올려 메모리 공간 확보하고 주솟값을 변수 sum에 연결
  • 그 다음 변수 선언부를 끌어올려 메모리 공간을 확보하여 주솟값 변수 multiply에 연결함
  • **console.log(sum(1,2)) 에서 sum에 데이터 12를 할당하고 sum의 결과값 3을 출력**
  • **console.log(multiply(3,4))는 변수 선언부 var multiply에 데이터 3,4를 할당했는데 multiply에는 어떠한 값도 할당되어 있지 않다.. 그래서 TypeError: multiply is not a function 에러를 출력**
  • 런타임 종료 후 실행 컨텍스트 콜스택 제거

함수 선언문은 전체를 호이스팅한 것에 비해 함수 표현식은 변수 선언부만 호이스팅했다. 여기서 중요한건 함수도 하나의 값으로 취급 한다는 것이다.


함수 선언문의 위험성과 차이

  • A 개발자가 단순한 sum 함수로 단순한 a+b를 만들어서 여기 저기 호출해서 활용했었는데 B라는 초보 개발자가 x,y를 받아 x+y+=(x+y)를 반환하는 함수를 만들었고 B는 작성한 함수 이후에만 영향을 줄것이라고 생각해서 배포까지한다. A 개발자가 의도한 함수가 아닌 다른 문자열 반환을 해버리는 문제가 발생한다
  • 코드가 5000줄 이상이라고 생각하면 오류를 잡아내기 쉽지 않다 (sum은 에러 반환을 하지 않고 숫자 대신 문자열을 넘겨받아도 암묵적 형변환에 따라 오류없이 통과됌)
console.log(sum(1, 2));

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

var a = sum(1, 2);

function sum(x, y) {
  return x + "+" + y + "=" + (x + y);
}

var c = sum(1, 2);
console.log(c);
  • 따라서 상대적으로 함수 표현식이 안전하다
console.log(sum(3, 4)); // sum is not a function

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

var a = sum(1, 2);

console.log(a); // 3

var sum = function (x, y) {
  return x + "+" + y + "=" + (x + y);
};

var c = sum(1, 2); // 1+2=3
console.log(c);

(2) 스코프 , 스코프 체인 , Outer Environment Reference

  • 스코프 : 식별자에 대한 유효범위 ⇒ ES5까지 전역공간을 제외하면 함수에 의해서만 스코프가 생성됌 ( var 선언은 작용하지 않고 let,const,class,strict mode의 함수 선언에서만 역할 수행 , ES6는 구분하기 위해 함수 스코프 , 블록 스코프)
  • 스코프 체인 : 식별자의 유효범위를 안에서 바깥으로 차례대로 검색하는 것
  • Outer Environment Reference : 현재 호출된 함수가 선언될 당시의 Lexical Environmentfmf 참조

코드로 쉽게 이해 하자면

var a = "global scope";

var outer = function () {
  var inner = function () {
    console.log(a);
    var a = "inner function scope";
  };
  inner();
  console.log(a);
};

outer();
console.log(a);

위 코드의 실행 과정은

  • 전역 컨텍스트 실행
  • Environment Record에 aouter의 식별자를 저장
  • 그리고 나서 변수 a1 할당 , outer에 함수를 할당
  • 근데 여기서 outer 함수가 호출되므로 전역 컨텍스트는 일시 중단(아까 할당했던 변수의 할당 된 순서보다 우선순위로 올라감)되고 outer()aouter의 식별자 다음 실행 컨텍스트로 활성화된다
  • 이제 outer 함수의 실행 컨텍스트에서 inner 식별자를 찾았으니 저장한다
  • inner()에 함수를 할당하고 inner 함수 호출 ⇒ outer 함수 실행 컨텍스트 일시 중단 ⇒ inner 함수 실행 컨텍스트 활성화
  • inner 함수 안에 있는 a 식별자를 저장한다
  • inner 함수 안에 있는 console.log(a)를 실행하면 아직 할당된 값이 없으므로 undefined를 출력한다
  • 이제 여기서 a 식별자의 Lexical Environment에 접근하고 Environ Record에 a를 찾고 없으면 Outer Environment에 있는 Environ Record를 찾는데 여기서 할당한 값이 없다
  • 그리고 inner 함수의 스코프에 있는 a'inner function scope'를 할당 ⇒ inner 함수 실행 종료 ⇒ 콜 스택 제거
  • 다시 outer 함수를 재활성화시키고 inner() 다음줄의 console.log(a) 실행
  • 이제 a 식별자의 Lexical Environment에 접근하고 Environ Record에 a를 찾고 없으면 Outer Environment에 있는 Environ Record로 넘어간다
  • 여기서 outer 함수엔 전역 Lexical Environment의 ‘global scope’를 할당했으니 이 데이터 저장값을 반환한다
  • outer 함수 실행을 종료 ⇒ 콜 스택 제거
  • 마지막 console.log에서 전역 컨텍스트를 다시 활성화 하고 전역 컨텍스트의 Environ Record에서 a를 검색하고 'global scope'찾아서 저장값을 반환한다
  • 전역 컨텍스트를 종료하고 콜 스택에서 제거한다

여러 스코프에서 동일한 식별자를 선언한 경우는 무조건 스코프체인 상에서 가장 먼저 발견된 식별자에만 접근 가능하다!

  1. 전역 공간에서는 전역 스코프에서 생성된 변수에만 접근 가능
  2. 따라서 outer 함수 내부는 전역 스코프 , outer 함수 스코프에는 접근 가능하지만 inner 함수 내부 스코프는 접근 할 수 없다
  3. **inner 함수 내부에서는 inner, outer ,전역 스코프에 모두 접근 가능하다.**
var a = "global scope";

var outer = function () {
  var inner = function () {
    console.log(a);
  };
  inner();
  console.log(a);
};

outer();
console.log(a);

// global scope
// global scope
// global scope
// a를 선언하지 않을땐 a가 전역 스코프에 접근 한걸 볼 수 있다.
  1. 그치만 inner 함수에서 a에 접근했을때 undefined가 뜬 이유는 inner 스코프 안에서 a라는 식별자가 이미 존재했고 inner 함수 안에 있는 Lexical Environment 상의 a를 반환 하게 된것이다.
  2. 그래서 전역 공간에서 선언한 동일한 이름의 a 변수에는 접근할 수 없다
  3. 이걸 변수 은닉화라고한다

스크린샷

크롬 개발자 도구로 스코프 정보를 확인 할 수 있다.


여기서 전역변수과 지역변수가 뭐냐면

  • 전역변수(Global Varible) : a와 outer가 전역 변수
  • 지역변수(Local Varible) : outer() 내부에서 선언한 inner, inner 함수 내부에서 선언한 a

쉽게 전역 공간에서 선언한 변수는 전역 변수이고 함수 내부에서 선언한 변수는 무조건 지역 변수다.