본문 바로가기

항해99_10기/105일의 TIL & WIL

[20221120] JavaScript 호이스팅과 TDZ (변수은닉화 내용 추가 예정)

스코프, 호이스팅, TDZ


스코프 (scope)

스코프는 현재 실행 중인 문맥을 지칭하는 단위로, 값과 표현식이 "보이"는 혹은 "레퍼"되는 공간을 의미함. 스코프에는 계층이 존재하며, 자식 스코프는 부모 스코프의 변수와 표현식을 레퍼할 수 있지만, 반대는 불가능함

  • 컴퓨터 프로그래밍에서 스코프는 name binding을 가능하게 하는데, 실제 존재하는 값을 지칭하는 이름을 만드는 것을 의미함
  • 이러한 스코프는 프로그램 내에서 이름 충돌을 방지하며, 각기 다른 스코프에서 같은 이름을 사용해 서로 다른 값을 지칭하게 해줌
  • The term "scope" is also used to refer to the set of all name bindings that are valid within a part of a program or at a given point in a program, which is more correctly referred to as context or environment.[a]
 

Scope (computer science) - Wikipedia

From Wikipedia, the free encyclopedia Jump to navigation Jump to search Part of a computer program where a given name binding is valid In computer programming, the scope of a name binding (an association of a name to an entity, such as a variable) is the p

en.wikipedia.org

 

자바스크립트에서 스코프의 종류

  • Global scope : 스크립트 모드에서 실행되는 전체 코드의 디폴트 스코프
  • Module scope : 모듈 모드에서 실행되는 코드의 스코프
  • Function scope : 함수와 함께 생성되는 스코프
    • 함수는 스코프를 생성하므로, 함수 안에서 선언된 변수들은 함수 안에서만 exclusive하게 접근할 수 있음
    • 다른 함수 스코프나 바깥에서 접근이 불가능하지만, 함수의 바깥쪽 (global 또는 해당 함수의 부모 스코프)에서 생성된 변수는 함수 내부에서 접근이 가능
const y = "declared outside function";

function exampleFunction() {
  const x = "declared inside function"; // x can only be used in exampleFunction
  console.log("Inside function");
  console.log(x);
  console.log(y);
}

console.log(x); // Causes error
console.log(y);
  • Block scope : let 또는 const으로 선언된 변수가 속하는 스코프로, { } 중괄호 안(block)에 속하는 내용들이 실행되는 스코프
    • 블록은 let과 const만을 스코프 하는데, var의 선언은 블록 레벨에서 스코핑 하지 못함. 즉, 블록 안에서 var를 선언하면, 어디서든 접근이 가능
{
  var x = 1;
  const y = 1;
}
console.log(x); // 1
console.log(y); // ReferenceError: y is not defined

 

 

Scope - MDN Web Docs Glossary: Definitions of Web-related terms | MDN

The scope is the current context of execution in which values and expressions are "visible" or can be referenced. If a variable or expression is not in the current scope, it will not be available for use. Scopes can also be layered in a hierarchy, so that

developer.mozilla.org

 

 

호이스팅 (hoisting)

JavaScript에서 호이스팅(hoisting)이란, 인터프리터가 변수와 함수의 메모리 공간을 선언 전에 미리 할당하는 것을 의미
 

호이스팅 - 용어 사전 | MDN

JavaScript에서 호이스팅(hoisting)이란, 인터프리터가 변수와 함수의 메모리 공간을 선언 전에 미리 할당하는 것을 의미합니다. var로 선언한 변수의 경우 호이스팅 시 undefined로 변수를 초기화합니다

developer.mozilla.org

  • 대부분의 경우, "변수의 선언과 초기화를 분리한 후, 선언만 코드의 최상단으로 옮기는 작업"을 호이스팅이라고 부르는데, 이 때문에 변수를 정의하는 코드보다 사용하는 코드가 앞서 등장이 가능한 것임
    • var, let, const로 선언한 모든 변수는 호이스팅 됨
    • var의 경우, 호이스팅 시 변수를 undefined로 초기화 하지만, let과 const의 경우 선언만 호이스팅 되고, 초기화 되지 않음 (let과 const로 선언한 변수의 사용 코드를 선언 전에 실행할 경우 reference error가 뜨는 이유 / 더 자세한 설명은 뒤에 TDZ 부분 참고)
  • 변수의 선언과 초기화를 함께 수행하는 경우, 선언 코드까지 실행해야 변수가 초기화 된 상태가 됨
console.log(x) // undefined


var x = 1
console.log(x) // 1

console.log(y) // Uncaught ReferenceError: y is not defined
let y = 2

console.log(z) // Uncaught ReferenceError: z is not defined
const z = 3

 

함수 선언문과 표현식에서의 호이스팅 차이

  • 함수를 선언한 경우, JS는 해당 함수의 코드를 실행하기 전에 선언을 메모리에 할당하기(호이스팅) 때문에, 함수를 호출하는 코드를 함수 선언보다 먼저 배치해도 작동함 => 기본적으로 선언만이 호이스팅의 대상
  • 함수의 선언은 그 선언을 둘러싼 함수의 최상위 스코프나, 전역 범위로 호이스팅 됨
catName("클로이");

function catName(name) {
  console.log("제 고양이의 이름은 " + name + "입니다");
}

/*
결과: "제 고양이의 이름은 클로이입니다"
*/
  • 반면, 함수를 표현식으로 작성한 경우, 선언과는 달리 호이스팅되지 않음
console.log(notHoisted) // undefined
//even the variable name is hoisted, the definition wasn't. so it's undefined.
notHoisted(); // TypeError: notHoisted is not a function

var notHoisted = function() {
   console.log('bar');
};
  • 생각해 볼 거리 : 함수를 해쉬 방식의 오브젝트에서 선언했을 때는 메모리에 어떤 방식으로 호이스팅 될까?
    • 아래 실험에서는 object의 선언 전에 실행을 시도해 보았음. 결과로 찍힌 TypeError 메세지로 유추해보자면, object의 선언만이 호이스팅 된 상태에서, 또, 함수의 선언은 해당 함수를 둘러싼 최상위 스코프 내에서 호이스팅 되므로, 함수의 최상위 스코프인 obejct가 초기화 되기 전엔 선언조차 호이스팅 되지 않은 것으로 생각됨
var math = {
  'factit': function factorial(n) {
    console.log(n)
    if (n <= 1)
      return 1;
    return n * factorial(n - 1);
  }
};

math.factit(3) //3;2;1;


    

// obejct를 선언하기 전에 object 프로퍼티로 넣은 함수를 호출해 봄
math.factit(3) //Uncaught TypeError: Cannot read properties of undefined (reading 'factit')
var math = {
  'factit': function factorial(n) {
    console.log(n)
    if (n <= 1)
      return 1;
    return n * factorial(n - 1);
  }
};

 

 

TDZ (Temporal Dead Zone)

TDZ(Temporal Dead Zone)의 의미는 변수의 선언 전 접근을 금지한다는 것이다. 즉, 선언 전에는 아무것도 사용하지 말라는 의미가 담겨있다.

 

Don't Use JavaScript Variables Without Knowing Temporal Dead Zone

Temporal Dead Zone forbids the access of variables and classes before declaration in JavaScript.

dmitripavlutin.com

 

  • 호이스팅 부분에서 언급한 var와 let/const 변수를 선언 전에 접근할 때 나타나는 차이점이 바로 이 TDZ의 존재 때문임. JS에서는 종류와 상관 없이 모든 변수를 호이스팅하지만, var와는 달리, let과 const의 경우, 해당 변수의 선언문을 실행하기 전까지는 TDZ에 존재하므로, 변수 선언에 도달하기 전 실행하면, ReferenceError를 던지게 되는 것임
  • TDZ의 영향을 받는 구문 : let & const 변수, class 구문, constructor() 내부의 super() , 기본 함수 매개변수
// class 구문의 예
const myNissan = new Car('red'); // throws `ReferenceError`

class Car {
  constructor(color) {
    this.color = color;
  }
}

// constructor() 내부의 super() 
// 부모 클래스를 상속받았다면, 생성자 안에서 super()를 호출하기 전까지 this 바인딩은 TDZ에 있다.
class MuscleCar extends Car {
  constructor(color, power) {
    this.power = power;
    super(color);
  }
}

// Does not work!
const myCar = new MuscleCar(‘blue’, ‘300HP’); // `ReferenceError`


//기본 함수의 파라미터 예시
//아래의 예제에서는 전역변수 a의 초기화를 먼저 했지만, 
//a=a 구문은 함수 스코프에서 새로운 a 변수를 선언하였으므로, 오른쪽 a는 초기화 전이기 때문에 에러가 남
const a = 2;
function square(a = a) {
  return a * a;
}
// Does not work!
square(); // throws `ReferenceError`

//아래의 경우, init 변수를 먼저 선언/할당 한 상태에서 함수의 파라미터로 선언한 a의 값으로 init을 가져오므로 에러 없이 잘 됨
const init = 2;
function square(a = init) {
  return a * a;
}
// Works!
square(); // => 4
  • TDZ에 영향을 받지 않는 구문 : var 변수, function, import 구문
    • 함수의 경우, 선언하지 않고 표현식으로 쓴다면 호이스팅이 되지 않기 때문에, TDZ를 신경쓸 필요 없음. 하지만, 선언한 경우, 호이스팅이 되고, TDZ의 영향을 받지 않음. 여기에 더해, var 변수와는 달리, 함수의 경우 호이스팅 된 상태에서도 리턴 값을 반환함
    • import 구문이 호이스팅 되기 때문에, 스크립트의 싲가 부분에 모듈을 가져오는 것이 좋음
// 함수 예제
greet('World'); // => 'Hello, World!'
function greet(who) {
  return `Hello, ${who}!`;
}
// Works!
greet('Earth'); // => 'Hello, Earth!'


// 모듈 예제 
myFunction();
import { myFunction } from './myModule';
  • typeof 연산자를 활용하면, TDZ에 선언된 변수를 판별할 수 있음
    • 변수가 선언되지 않은 상태라면, undefined를 반환하고, 변수가 TDZ에 있다면 ReferenceError를 반환
typeof notDefined; // => 'undefined'

typeof variable; // throws `ReferenceError`
let variable;
  • 스코프 안에서의 TDZ
    • TDZ는 선언문이 있는 스코프 내에서만 작동함. 아래의 경우, variable 변수는 if문의 스코프에서 선언되기 때문에, 그의 상위 스코프인 doSomething() 스코프에서는 typeof 연산자가 undefined를 반환함
function doSomething(someVal) {
  // Function scope
  typeof variable; // => undefined
  if (someVal) {
    // Inner block scope
    typeof variable; // throws `ReferenceError`
    let variable;
  }
}
doSomething(true);

 

 

 

실행 컨텍스트와 콜 스택


 

Call Stack과 Execution Context 를 알아보자

이 글에서 Call Stack과 Execution Context에 대해 다룹니다. 각 용어는 한국어로 해석하지 않고 영어 그대로 표기합니다.

medium.com

 

실행 컨텍스트 (Execution context)

실행 컨텍스트는 자바스크립트 코드가 실행되는 환경을 의미하며, global과 function execution context가 있음

  • Global context : JS 엔진이 처음 코드를 실행하면 Global execution context가 생성되고, 생성과정에서 Window Obejct 또는 Global(Node.js에서) 전역 객체를 생성하는데, this가 해당 객체를 가리키도록 함
    • JS엔진은 처음 코드를 실행할 때 Global context를 단 한번만 생성함
  • Function Execution context : 자바스크립트 엔진은 함수가 호출 될 때마다 호출 된 함수를 위한 Execution Context를 생성ㅎ고, 모든 함수는 호출되는 시점에 자신만의 Execution Context를 가짐
// 아무것도 없지만, JS를 실행하면, window 객체가 생성됨
console.log(this) // Window {0: Window, window: Window, self: Window, document: document, name: '', location: Location, …}

// node에서 JS를 실행하면 global 객체가 생성됨
PS C:\Users\yena\Desktop\developement_workspace\sparta\0. hanghae\2wk_algorithm>
Welcome to Node.js v16.14.2.
Type ".help" for more information.
> this
<ref *1> Object [global] {
  global: [Circular *1],
  clearInterval: [Function: clearInterval],
  clearTimeout: [Function: clearTimeout],
  setInterval: [Function: setInterval],
  setTimeout: [Function: setTimeout] {
    [Symbol(nodejs.util.promisify.custom)]: [Getter]
  },
  queueMicrotask: [Function: queueMicrotask],
  performance: Performance {
    nodeTiming: PerformanceNodeTiming {
      name: 'node',
      entryType: 'node',
      startTime: 0,
      duration: 2805.8415999412537,
      nodeStart: 0.5974999666213989,
      v8Start: 2.8655999898910522,
      bootstrapComplete: 24.810099959373474,
      environment: 13.079399943351746,
      loopStart: 52.05329990386963,
      loopExit: -1,
      idleTime: 2729.2166
    },
    timeOrigin: 1668948817485.65
  },
  clearImmediate: [Function: clearImmediate],
  setImmediate: [Function: setImmediate] {
    [Symbol(nodejs.util.promisify.custom)]: [Getter]
  }
}

 

Execution Context 생성과정

  • Execution context는 두 단계로 나뉘어 생성
    1. Create Phase 
      1. Global Object를 생성
      2. this 객체를 생성 / 함수의 execution context 생성시, this 대신 argument 객체를 생성
      3. 변수와 함수를 위한 메모리를 준비
      4. 변수에는 undefined를 할당하고 (var 변수 경우만) 함수 선언문은 실제로 메모리에 할당 => Creation Phase에서 호이스팅이 일어남
        • TDZ의 영향이 미치지 않는 함수의 경우, 메모리의 참조 값이 할당 됨
    2. Execution Phase : 실제 코드를 실행하며 선언된 변수에 값을 할당 / 함수의 경우, 함수 내부에서 선언된 변수에 값을 할당
  • 결론 : JS엔진은 코드를 실행시키면 총 두번에 걸쳐 작업을 하는 것 같음. 한 번은 Creation Phase로, 스크립트 내 Global 영역에 있는 모든 변수와 statement의 선언을 메모리에 할당하고, 다시 코드를 한 줄씩 실행하면서 변수에 실제 값을 할당하고, 한 줄씩 실행하다가 inner scope를 만나면, 해당 스코프에 존재하는 선언을 모두 메모리에 할당하고 다시 스코프 내의 한 줄 한 줄을 실행하면서 변수에 실제 값을 할당한여 코드를 실행한 뒤 해당 스코프가 종료되면 다시 global 스코프로 돌아아 스크립트의 끝까지 execution phase를 진행
    • 이렇게 Execution Context를 생성하면서 Call Stack에 각 context를 저장하는데, 저장 순서와 원리는 아래를 참고

 

Call Stack

Call Stack은 코드가 실행되면서 생성되는 Execution Context를 저장하는 자료구조

  • JS엔진은 execution context를 생성하고 이를 Call Stack에 push함. global은 스크립트를 처음 실행할 때 한 번, 그리고 함수는 함수를 호출할 때마다 함수를 위한 execution context를 생성하므로 매번 call stack에 push 함
    • 함수의 실행 컨택스트는 호출 순서대로 콜 스택에 쌓이게 되며, JS 엔진은 가장 위에 위치한 함수를 실행하고 함수가 종료되면 콜 스택에서 해당 함수를 제거한 후 다음 순서에 위치한 함수로 이동 (Last In Firt Out)
    • 함수는 호출되지 않으면 콜 스택에 쌓이지 않음!!
function first() {
    console.log('first')
    second();
}

function second() {
    console.log('second');
}

first();

  • 함수의 Stack trace에서도 이를 참조
function firstDeclare(){
    throw Error("error");
}

function secondDeclare(){
    firstDeclare();
}

function thirdDeclare(){
    secondDeclare();
}

thirdDeclare();

/* 아래는 call stack에 쌓인 순서를 볼 수 있다 
test:2 Uncaught Error: error
    at firstDeclare (test:2:11)
    at secondDeclare (test:6:5)
    at thirdDeclare (test:10:5) -> 가장 먼저 실행되어 가장 아래 쌓였음
    at test:13:1
*/

 

 

스코프 체인과 변수 은닉화


스코프 체인 (scope chain)

 

자바스크립트 - 스코프 체인(scope chain)란?

스코프 체인(scope chain)이란? 스코프 체인(Scope Chain)은 일종의 리스트로서 전역 객체와 중첩된 함수의 스코프의 레퍼런스를 차례로 저장하고, 의미 그대로 각각의 스코프가 어떻게 연결(chain)되고

ljtaek2.tistory.com

스코프 체인은 일종의 리스트로 global 객체와 중첩된 함수 스코프의 레퍼런스를 차례로 저장하고, 각각의 스코프가 어떻게 연결되어 있는지 보여주는 것

 

  • 자바스크립트 엔진은 execution context의 생성과정에서 스코프 체인을 활용해 중첩 상태인 함수의 하위 스코프부터 상위 스코프, 그리고 전역 스코프까지 참조할 수 있음
  • console.dir(함수명) : 스코프 체인은 개발자 도구에서 감춰진 프로퍼티인 [[Scope]]를 통해 개발자 도구에서 확인할 수 있음

 

변수 은닉화 (hiding variable)

 

변수 은닉화란 직접적으로 변경되면 안 되는 변수에 대한 접근을 막는 것

 

  • 변수 은닉화를 이해하려면 클로저 개념을 먼저 공부해야 함
 

클로저 - JavaScript | MDN

클로저는 함수와 함수가 선언된 어휘적 환경의 조합이다. 클로저를 이해하려면 자바스크립트가 어떻게 변수의 유효범위를 지정하는지(Lexical scoping)를 먼저 이해해야 한다.

developer.mozilla.org

  • 클로저(closure)는 함수와 함수가 선언된 어휘적 환경의 조합
    • 아래의 예저에서는 함수를 콜하면 함수를 반환하는 makeAdder 함수를 정의한 후, add5와 add10 상수에 이를 호출하도록 선언함.
      • JS에서는 함수를 호출하면, 숨김 속성인 [[Environment]]를 이용해 자신이 어디서 선언됐는지를 저장하고, 함수 호출이 끝나면, garbage collection 대상이 되어 메모리에서 정리 됨
      • 아래와 같이 add5에서 makeAdder 함수를 호출하면, 내부의 무명함수가 호출될 때, [[Environment]]에 makeAdder 함수의 정보를 저장하고, 이를 통해 외부 변수에 접근할 수 있게 됨. -> 내부 무명함수에서는 x, y가 Environment에 저장된 외부 변수임.
      • 이때, makeAdder 함수가 갖는 Environment는 메모리에서 삭제가 됐지만, add5에 할당된 내부 무명함수는 여전히 add5.[[Environment]] 공간에 자신이 만들어진 렉시컬 스코프의 정보를 가지고 있음 (즉, makeAdder함수의 parameter x와 선언한 변수 y의 정보를 메모리에 저장하고 있음)
      • 그래서, add5(100)와 add10(100)을 콜했을 때, 같은 함수에 대해 같은 parameter를 넣었음에도 불구하고, 서로 다른 클로저를 형성하고 있으므로, 결과적으로 서로 다른 값을 반환 함 
function makeAdder(x){
    let y = 10
    return function (z) {
        y++
        return x+y+z
    }
}

const add5 = makeAdder(5)
const add10 = makeAdder(10)

console.log(add5(100)); // 116
console.log(add10(100)); // 121

 

 

변수은닉화 내용 추가 예정