1차 공부/JavaScript

호이스팅(hoisting)과 TDZ(temporal dead zone)은 무엇일까?

공대탈출 2022. 11. 19. 19:57

글쓰기에 앞서 글쓴이는 코딩에 미숙한 사람임을 알립니다.

더 구글링 해보면 더욱 더 자세한 내용이 공식문서 혹은 블로그에 정리 되어 있을 수 있습니다.


0123456789


스코프

스코프란 변수에 접근할 수 있는 범위를 의미한다.

JS에선 전역스코프와 지역스코프가있다.

전역스코프는 말 그대로 모든 곳에서 변수가 선언되어있어 어느곳에서든 접근 할 수 있는 것이고,

지역 스코프는 한정된 공간에 변수가 선언되어있어 한정적으로 변수에대해 접근할 수 있는 것이다.

var a = 1	//전역 스코프

function print() {	//지역(함수)스코프
	var a = 111
    console.log(a)
}
print()	//111 함수 스코프에 선언된 a는 111이기에 111이 출력됨.
console.log(a)	//1 console.log가 함수스코프 내에 존재하지 않으므로 전역스코프 a값인 1이 출력됨.

 

호이스팅

호이스팅이란 인터프리터가 변수와 함수의 메모리 공간을 선언 전에 미리 할당하는 것을 의미한다.

윗 문장만 보면 너무 어렵게 느껴지는데...

단순하게 설명하자면 var로 선언된 변수들은 코드가 실행될 때 undefined라는 값을 미리 할당받고,

let과 const로 선언된 변수들은 코드가 실행될 때 값은 존재하지 않지만, 변수의 선언은 이루어 지는 것이다.

 

여기서 값이 존재한다와 변수를 선언한다가 구분되어야 함을 알아야 한다.

선언하다 초기화하다
var a a = 10
let b b = 20
const c c = 30

적어도 내가 쓴 글에서는 선언한다와 초기화하다가 이렇게 의미한다는 것을 알고 읽도록 하자.

var로 선언한 변수는 호이스팅시 선언이 되고, undefined라는 값이 초기화되지만,

let과 const는 값에 undefined으로도 초기화되지않는 Temporal Dead Zone을 갖는다.

 

그럼 Temporal Dead Zone은 뭔데?

영문 그대로 잠시 죽은 공간 이라고 생각하면 된다.

 

일단 TDZ를 설명하기에 앞서 let에 관하여 먼저 알아보고 가도록 하자.

 

let이 var이랑 뭐가 다른데?

설명하기에 앞서 글만 읽으면 이해가 가지 않을 수 있어 몇가지 예시를 가져왔으니,

이해가 가지 않더라도 글을 먼저 읽고, 예시를 보고 다시 글을 읽어보도록 한다. 그럼 이해가 잘 간다.

 

우리가 주로 사용하는 let은 블록 명령문이나 let을 사용한 표현식 내로 범위를 제한하며 변수를 선언한다.

그러나 var은 변수를, 블록을 고려하지 않고 현재 함수(또는 전역스코프), 어디에서나 접근할 수 있는 변수를 선언한다.

 

let은 자신을 선언한 블록과 모든 하위블록을 스스로 스코프로 가진다.

반면에 var은 스코프가 '자신을 선언한 블록'이 아닌 자신을 포함하는 함수이다.

 

//var
function vartest() {
	var x = 1
    if (true) {
    	var x = 2
        console.log(x)	//2 func에서 x가 var로 선언되었으나, 한번더 선언되었으므로
	}
    console.log(x)	//2 맨 처음 x=1이 있으나 if문 안의 var가 자신을 포함하는 함수를 스코프로 가지므로
}
//let
function lettest() {
	let x = 1
    if (true) {
		let x = 2
        console.log(x)	//2 let이 if문 안에 재선언되었으므로
	}
    console.log(x)	//1 if문 안의 let x는 바인딩되었으므로 func밑의 x=1이 설정된다.
}
//var과 let의 차이
var x = 'global'
let y = 'global'
console.log(this.x)	//'global' x는 var로 선언되었기 때문에 전역인 this값에 포함되어있어 값이 출력된다.
console.log(this.y)	//undefined y는 let으로 선언되어 전역에 포함되지 않아 값이 없다고 나온다.
//TDZ를 모르니 이해가 안될 수도 있음
function test() {
	var foo = 33
    if (foo) {
    	let foo = (foo+55)	//Reference Err
    }
}
test();
//바깥 스코프의 var foo가 값을 가져 if 블록도 평가된다.
//하지만 어휘적 스코프로 인해, var foo 값은 블록 내에서 사용 할 수 없다.
//따라서 (foo+55) 표현식은 let foo의 초기화가 끝나지 않은,
//즉 TDZ의 내부이며 그래서 4번줄에서 Reference Err가 발생한다.

당연히 어려울것으로 생각하지만, TDZ까지 보고나서 다시 읽어보면 이해가 갈 것이다.(아직 재선언 남음)

 

재선언 : 같은 변수를 같은 함수나 블록 내에서 다시 선언하려고 시도하면 SyntaxErr가 발생한다.

if(x) {
    let foo;
    let foo;	//Syntax Err
}
//switch에서 일어나는 재선언 에러
let x = 1
Switch (x) {
	case 0 : 
    	let foo
        break
    case 1 :
    	let foo
        break
}

Switch에서 일어나는 재선언 에러는 블록을 배치해 막을 수 있다.

//switch에서 일어나는 재선언 에러 막기
let x = 1
Switch (x) {
	case 0 : {
    	let foo
        break}
    case 1 :{
    	let foo
        break}
}

 

기타 예제

//기타예제
var a = 1
var b = 2
if(a=1) {
	var a = 11
    let b = 22
    console.log(a)	//11, if문 내에서 다시 선언 되었기 때문
    console.log(b)	//22, if문 내에서 다시 선언 되었기 때문
}
console.log(a)	//11, if문 내에서 a가 var을 통해 전역변수로 11이 선언되었기 때문
console.log(b)	//2, if문 내에서 let으로 지역변수로 선언되어 if문 밖에 적용되지않음
		//따라서 전역 변수로 설정된 b=2가 출력됨

TDZ

앞에서 let과const는 TDZ에 영향을 받는다 하였다.

일단 예시를 보며 이해를 해보자.

//const 예시
pi;	//reference err, pi를 불러온 문구가 TDZ에 있기 때문.
const pi = 3.14

const는 호이스팅은 되지만, var처럼 undefined조차 초기화되지 않기 때문에 에러가 발생한다.

지금 TDZ는 일시적으로 죽은 공간, 즉 호이스팅에서 pi변수는 선언했지만, const pi = 3.14에서 값이 초기화 되기 때문에

에러가 발생하는 것이다.

let은 const와 비슷하니 따로 설명은 하지 않겠다.

 

두번째로  class도 TDZ에 영향을 받는다.

//TDZ에 영향을 받는 class
const myNissan = new Car('red')	//reference err
class Car {
	constructor(color){
    	this.color = color
	}
}

class Car가 밑에서 초기화 되기때문에 TDZ의 Car('red')는 에러를 발생 시킨다.

//TDZ에 영향을 받지 않게 class를 올리자!
class Car {
	constructor(color) {
    	this.color = color
    }
}
const myNissan = new Car('red')
console.log(myNissan)	//'red'

 

마지막으로 constructor()내부의 super()도 TDZ의 영향을 받는다.

constructor(){} 클래스의 인스턴스 객체를 생성하고 초기화 함
super() 부모클래스로부터 상속받은 필드나 메소드를 
자식클래스에서 참조하는데 사용한다.
//TDZ의 영향을 받는 constructor내부의 super()
class MuscleCar extends Car{
	constructor(color, power) {
    	this.power = power	//부모클래스를 상속받아 super()가 호출되기 전까지 this를 사용할 수 없다.
        super(color)		//this의 바인딩은 TDZ에 있기 때문
    }
}
const myCar = new MuscleCar('blue','300HP')	//reference err
//TDZ의 영향을 받는 constructor내부의 super()
class MuscleCar extends Car{
	constructor(color, power) {
		super(color)		//부모클래스를 호출하여 인스턴스를 초기화함
        this.power = power	//인스턴스가 준비되어 this 사용가능
    }
}
const myCar = new MuscleCar('blue','300HP')
console.log(myCar.power)	//'300HP'

 

기본함수 매개변수도 포함된다.

 

const a = 2
function square(a=a){	//기본매개변수 a는 선언 전에 a=a로 오른쪽에 사용되었다.
	return a*a	//기본매개변수는 선언 및 초기화 다음에 사용되어야 한다.
}			//이 경우 다른 변수로 선언하여 기본매개변수로 사용하여야 한다.
console.log(square())
const init = 2
function square(a=init) {
	return a*a
}
console.log(square())	//4

 

 

다음으로 TDZ의 영향을 받지 않는 것이다. var, function이 있다.

var은 선언하기 전 undefined로 초기화 되어있기 때문에 TDZ가 존재하질 않는다.

 

반면 function은 선언된 위치와 상관없이 동일하게 호출된다.

greet('World')	//'Hello World'
function greet(who) {
	return `Hello ${who}`	//${}사용으로 백틱` 씀
}
greet('earth')	//'Hello earth'

 

마지막으로 import도 TDZ에 영향을 받지 않고 호이스팅되기 때문에 JS파일 시작에서 디펜던시 모듈을 가져올때 사용한다.

myfunction()
import {myfunction} from './myModule'

 

TDZ에서의 typeof동작에 대해 알아보자

typeof(notDefined)	//undefined -> 선언되지않아 undefined값이 출력된다.

typeof(variable)	//reference err -> 선언됐으나 초기화되지않아 에러발생
let variable

 

이제 진짜 TDZ의 마무리로 스코프 안에서 TDZ의 동작에 대해 알아보자.

스코프로 둘러쌓이면 TDZ는 한정되어 존재한다.

//한정된 공간의 TDZ
function (dosomething) {
	typeof variable		//undefined
    if (someVal) {
    	typeof variable	//reference Err, 유일한 TDZ공간
        let variable
    }
}
dosomething(true)

결론 : TDZ에 관해 잘 이해하고, var은 절대 쓰지 말자

 

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

함수에서의 호이스팅은 함수선언문(선언적 함수)에서만 작용하고 함수표현식(익명함수)에서는 작동하지 않는다.

//함수 선언식 (선언적 함수)
function funcDeclation () {
	return 'enchovy'
}
console.log(funcDeclation())	//'enchovy'

//===========================================================
//=======================구분선==============================
//===========================================================

//호이스팅 되고, 범위는 함수가 선언된 곳이나 전역으로 가진다
funcDclation()	//'enchovy', 호이스팅되어 작동함
function funcDeclation() {
	return 'enchovy'
}
console.log(funcDeclation())	//'enchovy'
//선언적 함수에서만 호이스팅됨.
//함수 표현식 (익명 함수)
let funcExpression = function() {
	return 'enchovy'
}
console.log(funcExpression())	//'enchovy'
//==============================================
//====================구분선====================
//==============================================
console.log(funcExpression)	//let으로 선언되어 TDZ안에 있어 오류 발생
let funcExpression = function() {
	return 'enchovy'
}
console.log(funcExpression())	//'enchovy', TDZ밖에 있어 출력됨.

 

실행 컨텍스트와 콜스택

 

실행컨텍스트

실행 컨텍스트 (Excution Context) : 자바스크립트가 실행되는 환경을 의미한다. 대표적으로 두가지 타입이 있다.

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

   자바스크립트의 동적언어로서의 성격을 가장 잘 파악할 수 있는 개념이다.

 

1) Global Excution Context

 : 자바스크립트 엔진이 처음 코드를 실행할 때 Global Excution Context가 생성된다.

   생성과정에서 전역 객체인 window Object를 생성하고(node는 Global) this가 window객체를 가리키게 한다.

 

2) Function Excution Context

 : 자바스크립트 엔진은 함수가 호출될 때마다 호출된 함수를 위한 Excution context를 생성한다.

   모든 함수는 호출되는 시점에 자기만의 Excution Context를 갖는다.

 

자바스크립트는 실행 컨텍스트가 활성화 되는 시점에 다음과 같은 현상이 일어난다.

-1 호이스팅이 발생한다.

-2 외부 환경 정보를 구성한다.

-3 this값을 설정한다.

 

콜스택

콜스택 (call stack) : 코드를 실행하면서 생성되는 Excution Context를 저장하는 자료구조

1)엔진이 처음 Script를 실행할 때 Global Excution Context를 생성하고, 이를 callstack에 push(저장)한다.

2) 엔진이 함수를 호출할 때마다 함수를 위한 Excution Context를 생성하고 이를 callstack에 push한다.

3) 자바스크립트엔진은 callstack의 Top에 위치한 함수를 실행하며 함수가 종료되면 stack에서 제거(pop)하고

    제어를 다음 Top에 위치한 함수로 이동한다.

 

function multiply(x,y) {
	return x*y
}
function printSquare(x) {
	let s = multiply(x,x)
    console.log(s)
}
printSquare(5)

스코프체인

스코프체인은 일종의 리스트로서 전역 객체와 중첩된 함수의 스코프의 레퍼런스를 차례로 저장하고,

의미 그대로 각각의 스코프가 어떻게 연결(chain)되고 있는지 보여주는 것이다.

 

예시와 설명, 사진을 보고 이해하도록 하자.

var v = '전역변수'
function a() {
	//function a Excution Context
    var v= '지역변수'
    function b() {
    	//function b Excution Context
        console.log(v)
	}
    b()
}
a()
			//a의 Excution Context안에 v가 '지역변수로 존재하여 '지역변수'출력
			//4번문의 '지역변수'가 없다면 전역객체의 v값인 '전역변수'출력

GEC가 쌓이고 funa()가 쌓인다. 그 뒤 funb()의 EC가 쌓이고, b()함수가 실행컨텍스트 내에서 변수 v를 탐색하는데,

v가 없으면 b()를 감싼 외부함수 a()를 탐색한다. a()에 v가 있으면 그것을 참조하고, 없다면 전역객체를 탐색한다.

전역객체에서도 찾지 못하면 v is not defined err를 출력한다.

하지만 만약 찾았다면 a()안의 v인 '지역변수'를 출력한다. a()안의 v를 제거하면 전역객체의 v인 '전역변수'를 출력한다.

이런 과정들이 스코프에 담긴 순서대로 탐색하는 스코프 체인이다.

 

변수은닉화

변수 은닉화란 외부 객체로부터 속성값을 감추는 특성을 의미한다.

(function () {
	var a = a
}) ();
console.log(a)		//a is not defined

이러한 방식으로 직접적으로 변경되면 안되는 변수에 대한 접근을 막는 것을 변수 은닉화 라고한다.

 

 

 

 

'1차 공부 > JavaScript' 카테고리의 다른 글

reduce()  (0) 2022.11.22
실습과제  (0) 2022.11.19
JavaScript 객체와 불변성이란?  (0) 2022.11.19
JavaScript의 자료형과 JavaScript만의 특성은 무엇인가  (0) 2022.11.19
변수 선언과 대입연산자, 자료형  (0) 2022.11.19