방춘덕(고양이 키우면 지을 이름)의 개발 블로그입니다.
closure 본문
먼저 이 글을 읽기 전에 이 전에 작성했던 scope에 관해서 꼭 읽어야만 한다.
목차
- closure란?
- first class object (일급 객체)
- closure를 활용해 모듈 만들어보기
- 자주 할만한 실수 들
- JS의 모든 함수는 closure?
1. closure란?
closure란 MDN에서는 "클로저는 함수와 함수가 선언된 어휘적 환경의 조합이다. (A closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment))"라고 표현한다.
아래 코드를 보며 이해해보자.
function greeting() {
var greetingText = "Hello"
function talk() {
console.log(greetingText)
}
return talk
}
var talkHello = greeting()
talkHello()
대부분의 언어에서는 함수를 벗어나면 스택 메모리가 해제되므로, greetingText가 null이라고 예상하겠지만, JS는 다르다.
이는 함수의 실행이 종료되면 function execution context가 사라지는 것은 명백한 사실이지만, 그 안의 activation object는 그대로 남아있기 때문에 greeting 함수가 종료된 뒤 talk 함수에서 greetingText에 접근할 수 있는 것이다. 이때, 남아있는 변수들을 자유 변수(free variable)이라고 부른다.
이러한 자유 변수는 function execution context 내부의 [[scope]]라는 internal property에 기록되며, 이를 통해 자유 변수에 대한 값을 가져온다.
즉, MDN의 정의와 같이 선언된 lexical environment를 기억하고 실행하는 함수라고 생각하면 될 것 같다.
반대로 lexical environment를 기억하고 실행하지 않는다면, 그 함수는 closure라 볼 수 없다.
2. first class object (일급 객체)
일급 객체는 프로그래밍 언어에서 존재하는 것으로, 아래의 조건을 만족하면 일급 객체라고 말할 수 있다.
-
변수에 담을 수 있다.
-
인자로 전달할 수 있다.
-
반환 값으로 전달할 수 있다.
갑자기 이 얘기를 꺼내는 이유는 JS에서 함수도 일급 객체이기 때문이다.
아래 예시를 봐보자
var integers = [7, 4, 1, 8, 9, 2]
var ascendingSort = function(number1, number2) {
return number1 - number2
}
integers = integers.sort(ascendingSort)
console.log(integers)
간단한 오름차순 정렬이다. 잘 보면 함수를 변수에 저장하고 이후 함수의 매개 변수로 사용하는 것을 알 수 있다. (흔히 고차 함수(HOF, Hight Order Function)라고 부르지만, 나중에 이 주제에 관해 글을 따로 작성할 것이다.)
즉, closure는 1급 객체이면서 값이 함수인 인수로도 볼 수 있다.
3. closure를 활용해 모듈 만들어보기
사실 이론도 중요하지만 과연 어떻게 유용하게 사용할 수 있는지 또한 관건이다.
closure를 사용해 간단한 counter 모듈을 작성하여 어떻게 하면 전역 상태를 오염시키지 않고 모듈을 만들 수 있는지 알아볼 것이다.
(function () {
var someData = 'some data'
console.log(someData) // some data
})()
console.log(someData) // Uncaught ReferenceError: someData is not defined
위의 코드는 흔히 즉시 실행 함수 (IIFE, Immediately Invoked Function Expression)라고도 한다. someData는 익명 함수 안의 변수이므로, 외부에서 접근이 불가능하다. 따라서 변수를 private화 시킬 수 있다. 이 개념은 함수에도 마찬가지로 적용된다.
위의 코드를 조금 더 변형시켜 아래와 같이 사용해볼 수 있다.
var increase = (function () {
var count = 0
return function () {
count += 1
return count
}
})();
increase() // count -> 1
increase() // count -> 2
increase() // count -> 3
console.log(count) // Uncaught ReferenceError: counter is not defined
전역 변수를 전혀 오염시키지 않고 숫자를 카운트하는 closure를 만들었다. 여기서 조금만 더 응용해보자.
<section>
<button id="increase-button">increase number</button>
<span id="count">0</span>
</section>
var count = document.getElementById('count')
var increaseButton = document.getElementById('increase-button')
function increase() {
var count = 0
return function () {
count += 1
return count
}
}
increaseButton.onclick = increaseButton
이제 increaseButton을 누를 때마다. 우리는 숫자가 1씩 증가하는 것을 확인할 수 있을 것이다. 하지만, increase함수를 호출할 때마다 모두 같은 count라는 변수를 공유하므로, 만약 카운팅 기능이 여러 개 필요한 코드에서는 알맞지 않다.
마지막으로 각 객체 별로 상태를 가지고 카운팅을 하는 기능을 만들어보자.
function makeCounter() {
var count = 0
return {
increase: function () {
return ++count
},
decrease: function () {
return --count
},
value: function () {
return count
}
}
}
var counter1 = makeCounter()
var counter2 = makeCounter()
counter1.increase()
counter2.decrease()
console.log(counter1.value()) // 1
console.log(counter2.value()) // -1
위와 같이 clousre를 잘 사용하면 전역 상태를 오염시키지 않고 기능을 모듈화 시켜 객체 지향적인 프로그래밍이 가능하다.
위와 같은 방식을 모듈 패턴이라고 부른다.
4. 자주 할만한 실수 들
가장 대표적으로 다들 예시를 드는 내용이 바로 for문 안에서의 closure를 생성하는 것이다.
var functions = []
for (var i = 1; i < 10; ++i) {
const multiplyNumber = function () {
return i * (i + 1)
}
functions.push(multiplyNumber)
}
for (const multiplyNumber of functions) {
console.log(multiplyNumber()) // 132, 132, 132 ...
}
이상하다. 값이 항상 i가 10일 때의 결과만을 나타내고 있다. 그 이유는 위에서 사용한 i가 전역 변수이기 때문이다. 여기에는 몇 가지 해결방법이 존재한다.
방법 1. i를 지역 변수로 만든다.
var functions = []
for (let i = 1; i <= 10; ++i) {
const multiplyNumber = function () {
return i * (i + 1)
}
functions.push(multiplyNumber)
}
for (const multiplyNumber of functions) {
console.log(multiplyNumber()) // 2, 6, 12 ...
}
ES5의 const와 let은 block statement로, 해당 블록 내에서만 존재하기 때문에 전역 값을 오염시키지 않기 때문에 올바른 결과가 출력된다.
2. 함수를 한번 더 감싸 실행한다.
var functions = []
for (var i = 1; i <= 10; ++i) {
const multiplyNumber = (function (number) {
return function () {
return number * (number + 1)
}
}(i))
functions.push(multiplyNumber)
}
for (const multiplyNumber of functions) {
console.log(multiplyNumber()) // 2, 6, 12 ...
}
즉시 실행 함수가 실행된 뒤 종료되면, function execution context가 사라지고 즉시 실행 함수의 AO만이 남는다. 안쪽의 계산을 진행하는 함수는 호출 시의 AO를 가진 상태로 multiplyNumber에 저장된다.
5. JS의 모든 함수는 closure?
JS의 모든 함수 또한 일급 객체이고, 내부적으로 자유 변수들을 추적하기 위해 [[scope]]라는 internal property을 가진다. 따라서, 사실상 JS의 모든 함수는 closure라고 정의할 수 있다.
하지만.. 글을 마무리하기 전 매우 흥미로운 질문과 답변을 읽게 되었다.
질문의 내용은 다음과 같다.
질문 1. Function 생성자로 생성한 함수도 closure 인가요?
var x = 10;
function createFunction1() {
var x = 20;
return new Function('return x;'); // this |x| refers global |x|
}
function createFunction2() {
var x = 20;
function f() {
return x; // this |x| refers local |x| above
}
return f;
}
var f1 = createFunction1();
console.log(f1()); // 10
var f2 = createFunction2();
console.log(f2()); // 20
답변 내용에 링크되어 있는 내용을 보고 MDN에 검색하여 그 내용을 가져와보았다.
Function 생성자로 생성된 함수는, 해당 creation context에 대해서 closure를 생성하지 않는다. 해당 함수들은 모두 전역 범위에서 생성되며, 실행할 때는 생성자가 작성된 범위의 변수가 아닌 로컬 및 전역 변수에만 접근할 수 있다.
질문 2. 내부 함수에 사용 가능한 자유 변수가 없는 경우에도 여전히 closure인가요?
const temp = function () {
var a = 10
return function () {}
}
이 경우에서는 개인별로 정의하는 구분이 다른 것 같다. 외부에서 자유 변수에 접근하는지에 대한 유무 (lexical environment를 기억하고 실행하는지에 관한 유무)를 중점으로 대화하고 있다.
찬성 측(closure가 맞다.)
lexicalScope()
function lexicalScope() {
var message = "This is the control. You should be able to see this message being alerted."
regularFunction()
function regularFunction() {
alert(eval("message"))
}
}
위 코드를 실행시키면 This is ~~~ 에 관련된 내용이 alert으로 나타난다.
반대 측(closure가 아니다.)
https://stackoverflow.com/questions/8665781/about-closure-lexicalenvironment-and-gc/8667141#8667141
블로그에 적기보단, 답변의 스크린샷을 같이 보며 이해하는 게 더 쉬울 것 같다.
이 글을 작성할 때 읽은 감사한 자료들.
https://javascriptweblog.wordpress.com/2010/10/25/understanding-javascript-closures/
https://developer.mozilla.org/ko/docs/Web/JavaScript/Guide/Closures
http://dmitrysoshnikov.com/ecmascript/chapter-6-closures/
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function
'JS' 카테고리의 다른 글
polyfill과 transpiler (0) | 2020.01.04 |
---|---|
event bubbling, capturing, delegation (0) | 2019.12.27 |
prototype과 prototype chain (0) | 2019.12.16 |
scope (1) | 2019.12.13 |
event loop (0) | 2019.12.11 |