[JS] Closure

클로저의 lexical scope을 이해하자

개념

  • MDN says :

    Closures are functions that refer to independent (free) variables (variables that are used locally, but defined in an enclosing scope). In other words, these functions ‘remember’ the environment in which they were created.

  • 여기서 핵심은 ‘remember’ the environment in which they were created, 생성될 시점에서의 환경을 기억한다라는 말이다. 런타임 시점에서 동적으로 상태가 바뀌어도 함수가 선언될 당시의 상태를 기억한다는 뜻이다.

  • ‘Lexical Scope를 따른다’라고도 표현이 가능한데 이 말이 무슨 말이냐면
const name = 'tuna'
function sayName() {
	console.log(name);
}

function sayAnotherName() {
	const name = 'anotherTuna';
	sayName();
}

sayName();			// tuna
sayAnotherName();	// tuna
  • sayAnotherName() 함수를 실행시켜도 ‘anotherTuna’가 출력되지 않는 이유는 sayName() 함수가 선언될 당시의 name은 ‘tuna’이기 때문이다. 즉, 동적으로 변수를 둘러싼 환경이 바뀌어도 영향을 받지 않는다.

기본 예제

  • MDN에 나온 다른 예제를 한 번 보자.
function init() {
  const name = "tuna"; // name은 init 에 의해 생성된 지역변수다
  function displayName() { // displayName() 은 내부 함수이며,클로저다
    console.log(name); // 부모 함수에서 선언된 변수를 사용한다
  }
  displayName();
}
init();	// tuna
  • 여기서 알 수 있는 사실은
    1. 자신의 지역 변수가 없음에도 displayName() 함수는 name을 출력한다
    2. 내부 함수는 외부 함수에 접근할 권한이 있기 때문이다
    3. 만약 displayName() 함수 안에 동일한 name 변수가 있다면 내부 변수가 우선적으로 사용된다
  • displayName()은 init() 함수 안에 속하기 때문에 init()의 스코프를 저장하고, 렉시컬 스코프 체인을 통해 name을 참조한다.

  • 근데, TOAST에 설명된 클로저는 이것을 우리가 부르는 클로저와 거리가 있다고 말한다. 다른 함수 안에서 정의되고 실행되었을 뿐, 밖으로 나오지 않았기 때문이란다.

  • 우리가 흔히 부르는 클로저의 전형은 다음 예제와 같다고 한다.
var color = 'red';
function foo() {
    var color = 'blue'; // 2
    function bar() {
        console.log(color); // 1
    }
    return bar;
}
var baz = foo(); // 3
baz(); // 4
  1. bar를 color를 찾아 출력하는 함수로 정의
  2. bar는 outer environment의 참조로 foo의 environment를 저장
  3. bar를 global의 baz라는 이름으로 데려옴
  4. bar는 자신의 스코프에서 color를 찾음
  5. 없다. 대신 체이닝된 outer environment를 찾아감
  6. foo에는 color가 있음. 값이 blue
  7. 그래서 blue 출력
  • 핵심 요약 : JS의 스코프는 렉시컬 스코프를 따른다

결과를 예측해보자

function count() {
    var i;
    for (i = 1; i < 10; i += 1) {
        setTimeout(function timer() {
            console.log(i);
        }, i*100);
    }
}
count();
  • 의도한 결과 : 1부터 9까지 출력
  • 실행 결과: 10만 9번 출력

  • 왜 그런지 생각해본다면
    1. timer() 함수의 상위 스코프는 어디일까? count() 함수다.
    2. i값을 자기가 가지지 않고 있으니까 상위 스코프인 count()에서 찾겠네?
    3. 근데 컴퓨터는 무지 빠르다. i는 이미 첫번째 100ms의 시간이 흐르기 전에 10이 되어있다
    4. timer()는 충실히 i값인 10을 9번 반복한다

  • 의도한 대로 출력을 하려면 두 가지 방식이 있는데
    1. 새로운 스코프를 추가해 반복 시마다 값을 따로 저장하자
    2. ES6의 블록 스코프를 사용하자
  • 1번 방식
    function count() {
       var i;
       for (i = 1; i < 10; i += 1) {
           (function(countingNumber) {
               setTimeout(function timer() {
                   console.log(countingNumber);
               }, i * 100);
           })(i);
       }
     }
     count();
    
    1. timer() 함수의 상위 스코프는 function(countingNumber)의 즉시 실행 함수다.
    2. 즉시 실행 함수는 함수를 바로 정의하고 실행하므로, for문이 실행될 때마다 각각의 스코프를 가지게 된다
    3. 따라서 1부터 9까지 정상출력된다


  • 2번 방식
    function count() {
       'use strict';
       for (let i = 1; i < 10; i += 1) {
           setTimeout(function timer() {
               console.log(i);
           }, i * 100);
       }
     }
     count();
    

추가로 공부할 것

  • 클로저 활용 방안
  • 스코프 체이닝

Reference

 Date: 
 Tags:  JS