처음부터 차근차근

[코어 자바스크립트] 실행컨텍스트(Scope, Var, Object, Hoisting) 본문

Language/JavaScript

[코어 자바스크립트] 실행컨텍스트(Scope, Var, Object, Hoisting)

HangJu_95 2023. 6. 5. 21:05
728x90

실행 컨텍스트란??

실행할 코드에 제공할 환경 정보들을 모아놓은 객체이며, 실행 컨텍스트가 활성화되는 시점에 다음과 같은 일을 진행한다.

  1. 선언된 변수를 위로 끌어올리기(호이스팅)
  2. 외부 환경 정보를 구성
  3. this 값을 설정

이러한 특징으로 인해 JS는 타 언어랑 다른 특징이 발생.

Call stack에 대한 이해

이미지 출처 :  https://velog.io/@leejuhwan/스택STACK과-큐QUEUE

동일 환경에 있는 코드를 실행할 때 필요한 환경 정보들을 모아 컨텍스트를 구성하고 이것을 위에서 설명한 '스텍'의 한 종류인 Call stack으로 쌓아 올린다. 가장 위에 쌓여있는 컨텍스트와 관련된 코드를 실행하는 방법으로 코드의 환경 및 순서를 보장한다.

 

컨텍스트의 구성

1) 실행 컨텍스트를 구성할 수 있는 방법

  1. 전역공간
  2. eval()함수
  3. 함수(우리가 흔히 실행컨텍스트를 구성하는 방법)

2) 실행컨텍스트 구성 예시 코드

// ---- 1번
var a = 1;
function outer() {
   function inner() {
      console.log(a); //undefined
      var a = 3;
   }
   inner(); // ---- 2번
   console.log(a);
}
outer(); // ---- 3번
console.log(a);

 

3) 실행컨텍스트 구성 순서

코드 실행 → 전역 (in) → 전역(중단) + Outer(in) → Outer(중단) + inner(in)

다 넣으면

inner(out) + outer(재개) → outer(out) + 전역(재개) → 전역(out) → 코드 종료

 

※ 핵심

특정 실행 컨텍스트가 생성되는(또는 활성화되는) 시점이 콜 스택의 맨 위에 쌓이는(노출되는) 순간을 의미

+ 현재 실행할 코드에 해당 실행 컨텍스트가 관여하게 되는 시점을 의미

 

실행 컨텍스트 객체의 실체

1. VariableEnvironment(VE)

  • 현재 컨텍스트 내의 식별자 정보
  • 외부 환경 정보(=outer)
  • 선언 시점 LexicalEnvironment의 snapshot

2. LexicalEnvironment(LE)

  • VE와 동일하지만, 변경사항을 실시간으로 반영

3. ThisBinding

  • this 식별자가 바라봐야할 객체

VE vs LE

구성요소 

1. environmentRecord(=record)

  1. 현재 컨텍스트와 관련된 코드의 식별자 정보들이 저장
  2. 함수에 지정된 매개변수 식별자, 함수자체, var로 선언된 변수 식별자 등

-> 이때, 컨텍스트 내부를 처음부터 끝까지 쭉 훑어나가며 순서대로 수집한다.

 

2. outerEnvironmentReference(=outer)

  • outerEnvironmentReference는 현재 호출된 함수가 선언될 당시 lexicalEnvironment를 참조한다.

 

차이점

VE는 snapshot을 유지하지만, LE는 냅샷을 유지하지 않으며, 실시간으로 변경사항을 반영

→ 실행 컨텍스트를 생성할 때, VE 정보를 먼저 담은 다음, 이를 그대로 복사해서 LE를 만들고, 이후 주로 LE를 활용

 

environmentRecord(=record) : 식별자 정보들이 저장(record)

수집 대상 : 함수에 지정된 매개변수, 식별자, 함수 자체, var로 선언된 변수 식별자 등

컨텍스트 내부를 처음부터 끝까지 순서대로 훑어가며 수집 (코드가 실행되는것이 아님!)

 

hoisting

인터프리터가 변수와 함수의 메모리 공간을 선언하기 전에, 미리 할당하는 것을 의미.

변수 정보 수집 과정을 이해하기 쉽게 설명한 '가상 개념'

→ 간단히 요약하자면, "변수의 선언과 초기화를 분리한 후, 선언만 코드의 최상단으로 옮기는"것을 의미

 

var로 선언한 변수의 경우 호이스팅 시 undefined로 변수를 초기화하지만, let과 const로 선언한 변수의 경우 호이스팅 시 변수를 초기화하지 않는다.

 

hoisting 규칙

1. 매개변수 및 변수는 선언부를 호이스팅 합니다.

Ex)

< 적용 전 >

//action point 1 : 매개변수 다시 쓰기(JS 엔진은 똑같이 이해한다)
//action point 2 : 결과 예상하기
//action point 3 : hoisting 적용해본 후 결과를 다시 예상해보기

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

<매개변수 적용>

//action point 1 : 매개변수 다시 쓰기(JS 엔진은 똑같이 이해한다)
//action point 2 : 결과 예상하기
//action point 3 : hoisting 적용해본 후 결과를 다시 예상해보기

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

<호이스팅 적용>

//action point 1 : 매개변수 다시 쓰기(JS 엔진은 똑같이 이해한다)
//action point 2 : 결과 예상하기
//action point 3 : hoisting 적용해본 후 결과를 다시 예상해보기

function a () {
   var x;
   var x;
   var x;

   x = 1;
   console.log(x);
   console.log(x);
   x = 2;
   console.log(x);
}
a(1);

예상되는 결과는 1, undefined, 2로 예상되었지만, 실제로는 1,1,2가 출력되는 현상

 

2. 함수 선언은 전체를 호이스팅

Ex)

<적용 전>

//action point 1 : 결과 값 예상해보기
//action point 2 : hoisting 적용해본 후 결과를 다시 예상해보기

function a () {
   console.log(b);
   var b = 'bbb';
   console.log(b);
   function b() { }
   console.log(b);
}
a();

<호이스팅 적용>


//action point 1 : 결과 값 예상해보기
//action point 2 : hoisting 적용해본 후 결과를 다시 예상해보기

function a () {
   var b; // 변수 선언부 호이스팅
   function b() { } // 함수 선언은 전체를 호이스팅

   console.log(b);
   b = 'bbb'; // 변수의 할당부는 원래 자리에

   console.log(b);
   console.log(b);
}
a();

에러(또는 undefined), ‘bbb’, b함수 가 나올줄 알았지만, function b, ‘bbb’, ‘bbb’ 가 출력되는 것이 확인

 

함수 선언문, 함수 표현식

// 함수 선언문. 함수명 a가 곧 변수명
// function 정의부만 존재, 할당 명령이 없는 경우
function a () { /* ... */ }
a(); // 실행 ok

// 함수 표현식. 정의한 function을 별도 변수에 할당하는 경우
// (1) 익명함수표현식 : 변수명 b가 곧 변수명(일반적 case에요)
var b = function () { /* ... */ }
b(); // 실행 ok

// (2) 기명 함수 표현식 : 변수명은 c, 함수명은 d
// d()는 c() 안에서 재귀적으로 호출될 때만 사용 가능하므로 사용성에 대한 의문
var c = function d () { /* ... */ }
c(); // 실행 ok
d(); // 에러!

 

hoisting 시 함수 선언문, 함수 표현식 다른 점

 

예시를 통해 활용해보자.

console.log(sum(1, 2));
console.log(multiply(3, 4));

function sum (a, b) { // 함수 선언문 sum
   return a + b;
}

var multiply = function (a, b) { // 함수 표현식 multiply
   return a + b;
}

LE는 Record와 outer를 수집한다. 그 중, record를 수집하는 과정에서 hoisting이 일어나고, 변수명과 함수명을 쭉 끌어올려본 결과는 이렇다.

// 함수 선언문은 전체를 hoisting
function sum (a, b) { // 함수 선언문 sum
   return a + b;
}

// 변수는 선언부만 hoisting

var multiply;

console.log(sum(1, 2));
console.log(multiply(3, 4));

multiply = function (a, b) { // 변수의 할당부는 원래 자리
   return a + b;
};

함수 선언문과 함수 표현식에서의 차이는 분명하다.

- 함수 선언문은 전체를 hoisting

- 함수 표현식은 선언부만 hoisting

 

이것을 주의해야 되는 것은 다음 예시를 통해 알 수 있다.

 

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

// 함수 선언문으로 짠 코드
// 100번째 줄 : 시니어 개발자 코드(활용하는 곳 -> 200군데)
// hoisting에 의해 함수 전체가 위로 쭉!
function sum (x, y) {
   return x + y;
}

...
...

var a = sum(1, 2);

...

// 함수 선언문으로 짠 코드
// 5000번째 줄 : 신입이 개발자 코드(활용하는 곳 -> 10군데)
// hoisting에 의해 함수 전체가 위로 쭉!
function sum (x, y) {
   return x + ' + ' + y + ' = ' + (x + y);
}

...

var c = sum(1, 2);

console.log(c);

만약 함수 표현식으로 짰다면??

 

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

// 함수 표현식으로 짠 코드
// 함수 선언부만 위로 쭉!
// 이 이후부터의 코드만 영향을 받아요!
var sum = function (x, y) {
   return x + y;
}

...
...

var a = sum(1, 2);

...

// 함수 표현식으로 짠 코드
// 함수 선언부만 위로 쭉!
// 이 이후부터의 코드만 영향을 받아요!
var sum = function (x, y) {
   return x + ' + ' + y + ' = ' + (x + y);
}

...

var c = sum(1, 2);

console.log(c);

협업을 많이 하거나, 복잡한 코드일 수록, 전역 공간에서 이루어지는 코드 협업일 수록 함수 표현식을 활용하는 습관이 필수!

 

Scope, Scope chain

 

Scope란 : 식별자에 대한 유효 범위를 뜻한다.

- 대부분 언어에서 존재하며, JS에도 존재.

 

Scope Chain : 식별자의 유효범위를 안에서부터 바깥으로 차례로 검색해나가는 것

이미지 출처 : https://jess2.xyz/JavaScript/scope-chain-closure/

이제부터 outer의 의미를 알아보면

- 스코프 체인이 가능토록 하는 것(외부 환경의 참조정보)라고 할 수 있다.

 

Scope Chain을 조금 더 자세히 알아보면

outer는 현재 호출된 함수가 선언될 당시의 LexicalEnvironment를 참조

(그 당시의 환경 정보를 저장한다.)

text 예시를 들면

  1. 예를 들어, A함수 내부에 B함수 선언 → B함수 내부에 C함수 선언(Linked List)한 경우 어떻게 될까요?
  2. 결국 타고, 타고 올라가다 보면 전역 컨텍스트의 LexicalEnvironment를 참조하게 됩니다.
  3. 항상 outer는 오직 자신이 선언된 시점의 LexicalEnvironment를 참조하고 있으므로, 가장 가까운 요소부터 차례대로 접근 가능
  4. 결론 : 무조건 스코프 체인 상에서 가장 먼저 발견된 식별자에게만 접근이 가능
// 아래 코드를 여러분이 직접 call stack을 그려가며 scope 관점에서 변수에 접근해보세요!
// 어려우신 분들은 강의를 한번 더 돌려보시기를 권장드려요 :)
var a = 1;
var outer = function() {
   var inner = function() {
      console.log(a); // 이 값은 뭐가 나올지 예상해보세요! 이유는 뭐죠? scope 관점에서!
      var a = 3;
   };
   inner();
   console.log(a); // 이 값은 또 뭐가 나올까요? 이유는요? scope 관점에서!
};
outer();
console.log(a); // 이 값은 뭐가 나올까요? 마찬가지로 이유도!

이 예시에서, inner 함수 내부에서 a 변수를 선언했었기 때문에 전역 공간에서 선언한 동일한 이름의 a 변수에는 접근할 수 없다.

-> 변수 은닉화

// 전역 컨텍스트
var a;
var outer
a = 1;
outer();
console.log(a); // -> 1

// outer 컨텍스트
var inner;
inner();
console.log(a); // -> 내부에 a가 없기에, scope chain을 이용해 outer 변수 활용 -> 1

// inner 컨텍스트
var a;
console.log(a); // -> undefined
a = 3;

각각의 실행 컨텍스트는 LE 안에 record와 outer를 가지고 있고,

outer 안에는 그 실행 컨텍스트가 선언될 당시의 LE정보가 다 들어있으니 scope chain에 의해 상위 컨텍스트의 record를 읽어올 수 있다.