Closures
클로저란?
함수 번들과 이들이 참조하는 주변 상태 ( lexical environment
라 칭한다) 의 조합을 의미한다.
쉽게 말하자면, 클로저는 독립적인 (자유) 변수를 가리키는 함수로, 클로저 안에 정의된 함수는 만들어진 환경을 기억한다. 즉, 클로저를 통해 inner function scope 에서 outer function scope에 접근할 수 있으며, JavaScript 에서는 함수가 생성되는 시점에 클로저도 생성된다.
흔히 함수 내에서 함수를 정의하고 사용하면 클로저라고 한다. 하지만 대개는 정의한 함수를 리턴하고 사용은 바깥에서 하게된다.
이해를 돕기 위해 다음 코드를 보자.
1 | function getClosure() { |
위에서 정의한 getClosure()
는 함수를 반환하고, 반환된 함수는 getClosure()
내부에서 선언된 변수를 참조하고 있다. 또한 이렇게 참조된 변수는 함수 실행이 끝났다고 해서 사라지지 않았고, 여전히 제대로 된 값을 반환하고 있는 걸 알 수 있다. 여기서 반환된 함수가 클로저이다.
1 | var base = 'Hello, '; |
출력된 결과를 보면 text
변수가 동적으로 변화하고 있는 것처럼 보인다. 실제로는 text
라는 변수 자체가 여러 번 생성된 것이다. 즉, hello1()
과 hello2()
, hello3()
은 서로 다른 환경을 가지고 있다.
Lexical Scoping
Lexical Scoping
은 함수들이 중첩되어 있을 때, parser가 변수 이름을 어떻게 해석하는지에 대해 설명한다. 즉, lexical scope는 중첩 함수가 연속적으로 존재하는 그룹에서, inner function이 그의 parent scope의 변수를 포함한 자원들에 접근이 가능함을 의미한다. 결국 자식 함수들이 부모 함수가 실행되는 문맥에 어휘적으로 묶이는 것 을 의미한다. 이를 static scope
라고 칭하기도 한다.
이를 직관적으로 이해하기 위해 다음 코드를 보자.
1 | function grandfather() { |
위 예시에서 lexical scope은 forward 방향으로 작동한다는 것을 알 수 있다. 이는 child 함수가 실행될 때 해당 문맥에 name 변수가 포함되어 접근이 가능하다는 것을 의미한다. 하지만 반대로, backward로는 작동하지 않기 때문에 likes는 child의 부모 함수에서 접근이 불가능하다.
이는 같은 이름을 가졌지만 다른 실행 문맥을 가진 변수들이 위에서부터 아래로 코드가 작동하면서 실행 스택 (execution stack) 에 선행 값들 (우선순위) 을 가지고 있음을 의미한다. 이 때 같은 이름을 가진 변수의 경우 안쪽에 존재하는 함수 (실행 스택에서 위쪽에 존재하는 문맥, 함수) 일수록 높은 우선순위를 가진다.
다른 예시를 보자.
1 | function init() { |
위 코드에서,
outer(parent) function
은init()
이 되고,inner(child) function
은displayName()
이 된다.
init
함수가 지역변수인 name
과 displayName
함수를 생성한다. 이 때, displayName은 init 안에 정의된 inner function 이므로 init 내에서만 작동할 수 있다. 특히 해당 함수는 그만의 지역 변수를 생성하지 않는데, inner function의 경우 outer function의 변수에 접근할 수 있으므로 displayName은 그의 parent function인 init 함수의 지역변수인 name에 접근할 수 있다.
Closure
1 | function makeFunc() { |
위와 동일한 코드이지만, displayName이 실행되기 전 outer function에서 반환된다는 점이 다르다.
보통 함수의 실행이 끝나면 그 내부에 있는 지역변수에 더이상 접근이 불가한 것이 일반적인 경우이지만, 위 코드에서는 makeFunc의 실행이 종료되어도 name 변수에 접근이 가능하다.
그 이유는 JavaScript의 함수는 클로저를 생성하기 때문이다. 클로저는 앞서 말했듯이 함수와 해당 함수가 선언된 lexical environment을 아울러 정의하는데, 이 ‘환경’ 은 클로저가 생성된 그 시기의, in-scope에 해당하는 모든 지역변수를 포함한다.
따라서 위 코드의 경우, myFunc은 makeFunc이 실행될 때 생성된 displayName instance의 참조변수이며, displayName 인스턴스는 그의 lexical environment - name 변수가 존재하는 - 를 지속적으로 참조 (유지) 한다. 따라서, myFunc이 호출되었을 때 name 변수에 계속해서 접근 가능하므로 alert에 “Mozilla”가 전달된다.
또 다른 예시를 보자.
1 | function makeAdder(x) { |
위 코드에서는 하나의 인자 x 를 받는 makeAdder(x) 함수가 존재하고, 이는 또 다시 다른 하나의 인자 y 를 받아 x와 y의 합을 반환하는 함수를 반환한다. 결국 makeAdder는 함수 공장으로 작용하는데, 이는 특정 값(y) 을 인자(x) 에 더하는 함수를 생성한다.
위 예시에서는 2개의 함수가 생성되는데,
add5
: 하나는 5를 그의 인자에 더하는 함수이고add10
: 다른 하나는 10을 더하는 함수이다.
add5
와 add10
은 모두 클로저이다. 이들은 같은 body를 가진 함수를 공유하지만 다른 lexical environment를 가진다. 이는 add5
의 lexical environment는 x=5이고, add10
에서는 x=10 임을 의미한다.
Closure Scope Chain
모든 클로저는
- Local Scope (Own Scope)
- Outer Functions Scope
- Global Scope
를 가진다.
주의해야 할 부분은 outer function이 중첩된 함수 (중 하나) 일 경우에, 해당 함수의 scope에 접근하는 것은 그를 둘러싼 scope 또한 포함한다는 것 - 결국 함수 scope의 chain을 생성한다 - 이다.
1 | // global scope |
위 예시에서는 중첩 함수가 연속적으로 정의되어 있으며 이들은 모두 outer function scope에 접근이 가능하다. 이 때, 클로저는 모든 outer function scope에 접근 가능하다고 정의된다.
Reference
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures