본문 바로가기

javascript

자바스크립트 클로저와 호이스팅

클로저란 함수와 스코프 사이의 폐쇄적 관계이다. 좀 더 구체적으로 다음과 같이 정의해 보았다.

정의 1. 클로저란 독립된 스코프를 참조하는 함수다.

정의 2. 클로저란 내부함수에서 외부함수의 렉시컬 스코프를 참조하는 관계다.

굉장히 추상적이기에 실제코드와 사전개념을 학습해야 이해가 쉽다. 사전 개념으로서 스코프와 호이스팅을 먼저 이해해야 한다.


스코프란

스코프는 영어 뜻 그대로 범위이며, 자바스크립트는 함수레벨 스코프 var를 갖는게 특징이다. 함수레벨 스코프란 함수내에서 해당 변수의 범위가 미치는 것을 말한다. 즉, 함수내에서 var i = 10이 생성 된다면 함수 내에 중괄호가 있던 없던 어디에서든 var i 를 참조할 수 있는 것이다.

자바와 같은 언어에서는 블록레벨 스코프(중괄호 안에서 변수의 범위가 정해짐)를 갖는다. 블록레벨 스코프란 중괄호내에서 범위를 갖게 된다. 즉 중괄호 내에서 선언된 변수는 중괄호 바깥의 범위에서는 참조할 수 없다. 하지만, 자바스크립트도 ECMA2015(ES6)에 블록레벨 스코프를 가진 변수 let과 const가 추가되었다.

렉시컬 스코프란

클로저에서 렉시컬 스코프라는 용어가 등장하는데 렉시컬 스코프란 생성되는 순간을 기억하는 스코프를 뜻한다. 클로저에서 이 렉시컬 참조하게 되어 내부함수에서 종료된 외부함수의 변수를 참조할 수 있게 되는 것이다. 일단 이렇게 추상적으로 생각해두고 코드를 통해 알아보면 편하다.

 


클로저란

간단한 클로저의 예시이다.

function outterFunc() { 
  var name = "park" 
  return 
  function innerFunc(){ 
    console.log(name); 
  } 
} 
var innerFunc = outterFunc(); 
innerFunc(); // park

바깥 함수 outterFunc가 종료되었음에도 내부함수 innerFunc에서 outterFunc의 변수인 name을 참조하고 있다.

클로저에서 자주 실수 하는 예

위의 코드는 구글에서 검색해보면 자주 나오는 클로저의 실수로 유명한 코드이다. setTimeout의 콜백이 실행될 때 클로저이기에 렉시컬 스코프를 기억할것 같지만 var는 호이스팅 되어 있기에 사실상 전역변수인 것이다. 그렇기에 i 가 10까지 돌고 난 뒤에 콜백이 실행되고 이미 10이 되어 있는 i를 1초뒤에 출력하게 된다.

이를 수정하기 위해 두가지 방법이 있다.

수정방법1

아래와 같이 클로저를 한번 더 활용하여 i의 값이 변화할 때 마다 렉시컬 스코프를 생성해 주는 것이다. 렉시컬 스코프란 생성되는 순간의 환경을 기억하는 스코프이다.

function func() { 
  for (var i = 0; i < 10; i++) { 
    (function(i){ //즉시실행 함수가 실행되면서 이곳에 렉시컬 스코프가 형성된다 
      setTimeout(function() { 
      console.log(i) // 0, 1, 2, 3, 4 ,5, 6, 7, 8, 9 
     }, 1000) 
   })(i)
  }
}

수정방법2

let keyword를 사용한다.

function makeCounter() { 
  for(let i = 0; i < 10; i++){ 
    setTimeout(function(){ 
      console.log(i) // 0, 1, 2, 3, 4 ,5 6, 7, 8, 9 
     }, 1000) 
  } 
}

let은 지역변수이기에 for문이 돌때마다 고유의 렉시컬 스코프를 생성하게 된다. 그리고 setTimeout의 콜백에 이 고유한 렉시컬 스코프에 저장되어 있는 i의 값들 참조하여 출력할 수 있다.

 


호이스팅이란

변수나 함수가 선언되어 졌을때 선언부와 할당부가 분리되어 자신이 소속된 스코프의 최상단으로 끌어져 올라가는 것을 의미한다. 변수 호이스팅과, 함수 호이스팅으로 나눌 수 있다.

코드로 살펴보면 다음과 같다.

호이스팅 전

func(); 
function func(){ 
  var name = "park"; 
  var age = 29; 
  console.log("hello my name is" + name + "and my age is" + age) 
} 
var friend = "kim"

호이스팅 후

아래와 같이 선언부와 할당부가 분리되며 선언부만 위로 끌어올려진다.

var friend; // 변수 호이스팅 
function func() { // 함수 호이스팅 
  var name; // 기존 함수레벨 스코프 내에서의 변수 호이스팅 
  var age; // 기존 함수레벨 스코프 내에서의 변수 호이스팅 
  name = park; 
  age = 29; 
} 
func(); 
friend = "kim"