JavaScript에서 함수는, 선언될 때 자신이 접근할 수 있는 범위를 정하고 기억하고 있다.
이것을 lexical scope(렉시컬 스코프)라고 하는데, 이런 lexical scope에 의해
외부 함수의 환경을 기억하고 있는 내부 함수가 Closure다.
closure가 내부 변수를 참조하는 동안에는
내부 변수가 차지하는 메모리를 Garbage Collector가 회수하지 않으므로,
closure 사용이 끝나면 참조를 제거하는 것이 좋다.
왼쪽 | 오른쪽 |
---|---|
1. f1()이라는 함수가 정의되었다. 2. var a = 1; - 이 함수 내에는 함수 안에서만 사용할 수 있는 변수 a가 있다. 3. var a = f1(); 을 통해f1() 이 호출되어 함수가 실행되었고,return을 통해 값을 반환한다. 4. 값을 반환하고 함수가 종료되었기 때문에, 변수 a의 유효성도 종료가 됐다. 함수 내 변수의 생명주기는 함수 내에서 변수가 생성되고 return으로 종료되기 전까지이다. 함수가 종료되면 f1()이 가지고 있던 모든 자원들이 해제가 된다. (= Garbage Collector에 의해 수거된다.) | 1. f1() 함수를 호출해서 반환 받는 결과가 f2() 함수 객체(object)이다.2. var f = f1(); var a = f(); 을 통해 f1() 함수가 호출, 실행되고, 함수는 값을 return하며 종료된다.이 때 자신이 가지고 있던 모든 자원을 없애야 한다. 3. 그런데 값을 반환하는 f2() 함수에서 반환할 a값을 찾으려하니,그 a는 이미 종료된 함수 f1() 에 있다.4. return하는 함수 f2() 안에서lexical scope(렉시컬 스코프)에 의해 기억된 f1() 의 지역변수 a를 사용해야 하므로f1() 함수는 종료되었음에도 가지고 있던 자원을 없애지 못한다.var f 가 return값으로 null을 받거나, 다른 함수로 대체되지 않는 이상f1() 함수의 생명은 지속된다.(= Garbage Collector에 의해 수거되지 않고 메모리 상에 남아있는다.) 5. f2() 함수는 f1() 이 가진 자원을 닫을 수 있게 해주는 유일한 키워드, closure다. |
function Hello(name) {
this._name = name;
}
Hello.prototype.say = function() {
console.log('안녕, ' + this._name);
}
var hello1 = new Hello('해리포터');
var hello2 = new Hello('론위즐리');
hello1.say(); // '안녕, 해리포터'
hello2.say(); // '안녕, 론위즐리'
hello1._name = 'anonymous';
hello1.say(); // '안녕, anonymous'
Hello()로 생성된 객체 hello1과 hello2는 모두 name이라는 변수를 가지게 된다.
변수명 앞에 underscore()를 붙혀 '이 변수는 Private한 변수로 쓰고 싶다!'고 표기를 했지만
실제로는 여전히 외부에서도 쉽게 접근가능한 변수일 뿐이다.
외부에서 hello1._name = 'anonymous'을 통해
hello1() 함수 내 hello1._named에 'anonymous'을 할당했을 때,
그 값이 변경된 걸 보면 은닉화에 실패한 것을 알 수 있다.
function Hello(name) {
var _name = name;
return function() {
console.log('안녕, ' + _name);
};
}
var hello1 = Hello('해리포터');
var hello2 = Hello('론위즐리');
hello1(); // '안녕, 해리포터'
hello2(); // '안녕, 론위즐리'
return function() {
console.log('안녕, ' + _name);
};
Hello() 함수 내에 위와 같은 closure를 생성했다.
이제 hello1, hello2 함수를 호출, 실행 후 함수가 종료되면
hello1, hello2의 자원에 접근할 수 있는 것은 내부의 return되는 함수 뿐이다.
외부에서는 _name이라는 변수에 접근할 방법이 없다.
closure를 통해 변수_name을 Encapsulation(은닉화) 한 것이다.
for (var i = 0; i < 10; i++) {
setTimeout(function() {
console.log(i);
}, 100);
}
0-9까지의 정수를 출력하고 싶어 작성한 코드이다.
하지만 실제로 실행을 해보면 10만 열 번 출력된다.
단일 스레드 언어인 JavaScript는 콜스택에 작업을 push하고 처리할 때마다 하나씩 pop한다.
setTimeout은 비동기작업이기 때문에 콜스택이 아닌 이벤트 큐에 저장된다.
콜스택이 비어있는 순간, 이벤트 루프는 이벤트 큐에 쌓여있던 작업을 push하여
setTimeout이 받은 익명의 콜백 함수를 실행시킨다.
그런데 컴퓨터는 계산속도가 굉장히 빠르므로 0.1초가 채 되기전에 이미 반복문이 순회를 마친 상태다.
그 후 setTimeout의 콜백 함수가 호출되면서 이미 10이 된 i를 참조하게 되는 것이다.
for (var i = 0; i < 10; i++) {
(function(j) {
setTimeout(function() {
console.log(j);
}, 100);
})(i);
}
반복문 내의 함수를 즉시 실행함수(Immediately Invoked Function Expressions)로 만들어
setTimeout()에걸린 익명함수를 closure로 만들었다.
closure는 자신이 만들어진 환경을 기억한다.
closure가 생성될 당시 반복문을 돌며 i의 값이 바뀌고 있었고,
closure는 10번 반복된 10개의 서로 다른 환경을 기억하고 있다.
이 i를 parameter j로 받아오게 되면 값들이 동적으로 바뀌어도 반영이 되기 때문에,
비로소 우리가 원하던 결과를 얻을 수 있다.
+) 위 즉시 실행함수는 아래 코드처럼 작성해도 동일하게 동작한다.
for (var i = 0; i < 10; i++) {
function call(j) {
setTimeout(function () {
console.log(j);
}, 100);
}
call(i);
}
참고 자료
유튜브-1
유튜브-2
[자바스크립트] 클로저란?
[JavaScript] 자바스크립트 클로저의 의미와 사용하는 이유
JavaScript 클로저(Closure)