변수를 선언하고 초기화했을 때 선언 부분이 최상단으로 끌어올려지는 현상.
호이스팅의 대상
- var 변수 선언.
할당은 끌어올려 지지 않는다.( let, const 변수 선언은 호이스팅이 일어나지 않는다. )
아래 예시 코드를 보자.console.log(a); // (2) 출력 - undefined var a = 10; // (1) var a; 변수 선언, (3) a = 10; 변수 할당 console.log(a); // (4) 출력 - 10
주석의 괄호 안 숫자가 바로 코드가 실행되는 순서이다.
호이스팅이 발생해 var변수 선언이 최상단에 위치하게 되고,
이로 인해 첫번째 콘솔에는 undefined가 출력된다.
즉, 실제로 자바스크립트가 해석한 코드는 다음과 같다.var a; console.log(a); a = 10; console.log(a);
하지만 let과 const는 호이스팅이 발생하지 않으므로 에러가 발생한다.
console.log(a); // 에러 발생 let a = 10; console.log(a);
- 함수 선언문 (함수 표현식은 호이스팅이 일어나지 않는다.)
마찬가지로 할당은 끌어올려 지지 않는다.fun1(); // (3) 호출 fun2(); // (5) 호출 - TypeError: fun2 is not a function function fun1() { // (1) 함수 선언 console.log("I'm fun1()"); // (4) 출력 } var fun2 = function() { // (2) var fun2; 변수 선언 console.log("I'm fun2 "); }
과연 let, const 는 호이스팅이 일어나지 않을까?
엄밀히 말하자면 let, const도 자신이 속한 스코프의 최상단으로 호이스팅은 일어난다.
하지만 선언되어 초기화 되기 전까지 TDZ (Temporal Dead Zone) 영역에 속해 메모리가 할당되지 않아 참조에러(ReferenceError) 가 발생한다.
여기서 바로 var와 let,const 의 차이가 발생한다.
변수의 선언은 3단계로 구분이 되는데,
var
으로 선언된 변수는 선언과 초기화가 한번에 이루어지는 반면,
let
으로 선언된 변수는 선언과 초기화 단계가 분리되어 이루어진다.
이러한 이유로 var와 let,const 의 차이가 발생하는 것이다.
context 는 문맥 이라는 의미를 가지고 있다. JavaScript, TypeScript 의 실행 컨텍스트 는 코드의 문맥, 즉 코드의 실행환경 이라고 할 수 있다. 실행 컨텍스트는 이러한 환경에서 실행될 코드에 대한 모든 정보들을 모아둔 객체이다.
아래의 코드를 보자
var a = 10; // (1) 변수 선언, (3) 할당
fun1(); // (4) 함수 호출
function fun1 () { // (2) 함수 선언,
function fun2 () { // (5) 함수 선언
console.log(a); // (8) 변수 출력 - undefined
var a = 50; // (7) 변수 선언, (9) 할당 - 50
}
fun2(); // (6) 함수 호출
console.log(a); // (10) 출력 - 10
}
console.log(a); // (11) 출력 - 10
주석의 괄호 안 숫자가 코드가 실행되는 순서이다. 진짜 어지럽다.
차근차근 살펴보자.
처음 코드가 실행될 때 모든 것을 관리하는 환경인 전역 컨텍스트 가 생성된다.
이후, 함수 호출 시 마다 함수 실행 컨텍스트가 생성된다.
( 컨텍스트 생성 시, 컨텍스트 안에는 변수객체, scope chain, this가 생성된다 )
컨택스트 생성 후 함수가 샐행되는데, 사용되는 변수들은 해당 스코프 안에서 값을 찾고,
없다면 스코프 체인을 따라 올라가며 찾는다. (지역 변수라고 생각하면 된다)
함수 실행이 마무리되면 해당 컨텍스트는 사라지고, 모든 코드의 실행이 종료되면
전역 컨텍스트가 사라진다.
먼저 코드를 보고 실행 순서와 결과를 예측해보자.
var name = "JonDoe"; // (1) 변수 선언, (4) 할당
function sayHello(word) { // (2) 함수 선언
console.log(`${word}~! ${name}`); // (10) 출력
}
function say() { // (3) 함수 선언
var name = "Sam"; // (6) 변수 선언, (7) 할당
console.log(name); // (8) 출력
sayHello("Hello"); // (9) 함수 호출 - 함수 컨텍스트 생성
}
say(); // (5) 함수 호출 - 함수 컨텍스트 생성
전역 컨텍스트가 생성된 후, 변수 객체, scope chain, this가 들어온다.
(1) ~ (4) 까지 :
'전역 컨텍스트': {
변수객체: {
arguments: null,
variable: [{
name: 'JonDoe'
}, {
sayHello: Function
}, {
say: Function
}]
},
scopeChain: ['전역 변수객체'],
this: window,
}
(5) ~ (8) 까지 :
'say 컨텍스트': {
변수객체: {
arguments: null,
variable: [{ name: 'Sam' }],
},
scopeChain: ['say 변수객체', '전역 변수객체'],
this: window,
}
(9) sayHello("Hello") 는 say 컨텍스트 안에서 sayHello 변수를 찾을 수 없다.
따라서 scope chain을 따라 올라가 상위 변수객체인 전역 변수객체에서 찾는다.
전역 컨텍스트.변수객체.variable의 sayHello 함수를 호출한다.
(9) ~ (10)
'sayHello 컨텍스트': {
변수객체: {
arguments: [{ word : 'Hello' }],
variable: null,
},
scopeChain: ['sayHello 변수객체', '전역 변수객체'],
this: window,
}
console.log(
${word}~! ${name}
) 의 word는 arguments에서 찾을 수 있고,
name은 sayHello 변수 객체에 존재하지 않는다. 따라서 역시 scope chain 을 따라
상위 변수객체로 올라가고, 전역 변수객체의 variable의 name인 JonDoe가 출력된다.
sayHello 함수 종료 후, sayHello 컨텍스트가 사라지고, say 함수가 종료된다.
따라서 say 컨텍스트도 사라지고, 마지막 전역 컨텍스트 또한 사라진다.
console.log(name); // (3) 변수 출력
hoistFunction(); // (4) 함수 호출
function hoistFunction() { // (1) 함수 선언
console.log(a); // (6) 출력
var a = "variable"; // (5) 변수 선언, (7) 할당
console.log(a); // (8) 출력
}
var name = "James"; // (2) 변수 선언, (9) 할당
(1) ~ (3) 까지 :
'전역 컨텍스트': {
변수객체: {
arguments: null,
variable: [{
hoistFunction: Function
}, {
name: undefined
}],
},
scopeChain: ['전역 변수객체'],
this: window,
}
(4) ~ (8) 까지 :
'hoistFunction 컨텍스트': {
변수객체: {
arguments: null,
variable: [{ a: "variable" }],
},
scopeChain: ['hoistFunction 변수객체', '전역 변수객체'],
this: window,
}
hoistFunction(); // (3) 함수 호출
myFunction(); // (8) 함수 호출 - TypeError: fun2 is not a function
var myFunction = function() { // (1) 변수 선언
console.log("call Myfunction() Success!");
}
function hoistFunction() { // (2) 함수 선언 (및 초기화)
console.log(a); // (5) 출력
var a = "variable"; // (4) 변수 선언, (6) 할당
console.log(a); // (7) 출력
}
(1) ~ (2) 까지 :
'전역 컨텍스트': {
변수객체: {
arguments: null,
variable: [{
myFunction: undefined
}, {
hoistFunction: Function
}],
},
scopeChain: ['전역 변수객체'],
this: window,
}
(3) ~ (7) 까지 :
'hoistFunction 컨텍스트': {
변수객체: {
arguments: null,
variable: [{ a: "variable" }],
},
scopeChain: ['hoistFunction 변수객체', '전역 변수객체'],
this: window,
}
(8) myFunction 함수 호출 :
'전역 컨텍스트': {
변수객체: {
arguments: null,
variable: [{
myFunction: undefined
}, {
hoistFunction: Function
}],
},
scopeChain: ['전역 변수객체'],
this: window,
}
myFunction 함수가 대입되기 전에 호출하므로 TypeError: fun2 is not a function 발생.
클로저는 함수와 함수가 선언된 어휘적 환경의 조합이다. 클로저를 이해하려면 자바스크립트가 어떻게 변수의 유효범위를 지정하는지(Lexical scoping)를 먼저 이해해야 한다.
"lexical" 이란, 어휘적 범위 지정(lexical scoping) 과정에서 변수가 어디에서 사용 가능한지 알기 위해 그 변수가 소스코드 내 어디에서 선언되었는지 고려한다는 것을 의미한다. 단어 "lexical"은 이런 사실을 나타낸다. 중첩된 함수는 외부 범위(scope)에서 선언한 변수에도 접근할 수 있다.
아래의 코드를 보자.
var makeClosure = function() { // (2) 변수 선언, (3) 함수 할당
var name = '홍길동'; // (5) 변수 선언, 할당
return function () { // (6) 함수 반환,
console.log(name); // (8) 출력 - 홍길동
}
};
var closure = makeClosure(); // (1) 변수 선언, (4) 함수 호출, (6) function(){ console.log(name); }를 closure 변수에 할당
closure(); // (7) 함수 호출
(1) ~ (5) 까지 :
"전역 컨텍스트": {
변수객체: {
arguments: null,
variable: [{
makeClosure: Function
}, {
closure: undefined
}],
},
scopeChain: ['전역 변수객체'],
this: window,
}
"makeClosure 컨텍스트": {
변수객체: {
arguments: null,
variable: [{ name: '홍길동' }],
},
scopeChain: ['makeClosure 변수객체', '전역 변수객체'],
this: window,
}
(6) ~ (8) 까지 :
"closure 컨텍스트": {
변수객체: {
arguments: null,
variable: null,
},
scopeChain: [ 'closure 변수객체', 'makeClosure 변수객체', '전역 변수객체' ],
this: window,
}
var closure = makeClosure();
와 closure();
를 주목하자.
function(){ console.log(name); }
를 반환받은 closure
내부엔 자신만의 지역 변수가 없다.
그런데 함수 내부에서 scope chain을 통해 외부 함수의 변수에 접근할 수 있기 때문에 closure()
역시 부모 함수 makeClosure()
에서 선언된 변수 name
에 접근할 수 있다. 만약 closure()
가 자신만의 name변수
를 가지고 있었다면, name
대신 this.name
을 사용했을 것이다.
마지막으로 한가지 예시만 더 살펴보자.
var counter = function() { // (1) 변수 선언, (3) 함수 할당
var count = 0; // (5) 변수 선언, 할당
function changeCount(number) { // (6) 함수 선언, 할당
count += number;
}
return { // (7) 반환
increase: function() { // (7-1) 함수 선언
changeCount(1);
},
decrease: function() { // (7-2) 함수 선언
changeCount(-1);
},
show: function() { // (7-3) 함수 선언
alert(count);
}
}
};
var counterClosure = counter(); // (2) 변수 선언, (4) 함수 호출, (7) function changeCount(number) {} 반환, 할당
counterClosure.increase(); // (8) 함수 호출 ~~~~
counterClosure.show(); // 1
counterClosure.decrease();
counterClosure.show(); // 0
counter()
함수를 호출할 때, counterClosure 컨텍스트
에 counterClosure
와 counter
가 담긴 scope chain이 생성된다. 그렇게 되면 이제 counterClosure
에서 계속 count
로 접근할 수 있다. return
안에 들어 있는 함수들은 count 변수
나, changeCount 함수
또는 그것들을 포함하고 있는 스코프에 대한 클로저라고 부를 수 있다.
클로저는 프론트엔드 단에서 중요한 개념이므로 알아두도록 하자.
이 모든 정리는 결국 var 를 사용하지 말자는 것이다. 이유인 즉슨,,,