클로저(Closure)는 일급 객체 함수(first-class functions)의 개념을 이용하여 유효범위(scope)에 묶인 변수를 바인딩 하기 위한 일종의 기술이다. 기능상으로, 클로저는 함수를 저장한 레코드(record)이며, 스코프(scope)의 인수(Factor)들은 클로저가 만들어질 때 정의(define)되며, 스코프 내의 영역이 소멸(remove)되었어도 그에 대한 접근(access)은 독립된 복사본인 클로저를 통해 이루어질 수 있다.
자바스크립트 클로저(Closure)는 독립적인 변수(로컬로 사용되지만 둘러싼 범위에서 정의 된 변수)를 참조하는 함수입니다.
이 함수들은 생성될 당시의 환경을 기억 합니다.
이 방식으로 종료된 함수 내 특정 지역변수를 사용할 수 있습니다.
우선 간단한 예제를 살펴봅시다.
function plus() {
var a = 0;
return function () {
return ++a;
}
}
var p = plus();
console.log(p()); // 1
console.log(p()); // 2
console.log(p()); // 3
함수 plus
는 변수 p
에 할당되며 실행이 종료되었지만, p
를 실행하면 plus
함수 내에 선언된 변수 a
를 사용할 수 있습니다.
변수 a
가 사용되는 동안에는 GC이 되지 않습니다.
따라서 상황에 맞게 변수 a
참조를 없애주는 것이 좋습니다.
// 참조 제거
p = null;
배열 데이터에 값을 할당할 때 사용된 변수 i
는 호출 시점에서 접근하기 때문에, 출력 시 반복문이 종료된 상태의 i
의 값인 ‘5’만을 참조합니다.
var arr = [];
for (var i = 0; i < 5; i++) {
arr[i] = function () {
return i;
}
}
arr.forEach(function (el) {
console.log(el()); // 5 / 5 / 5 / 5 / 5
});
즉시실행함수(IIFE)로 값을 할당하여, 호출과 참조 시점을 같게 해줍니다.
var arr = [];
for (var i = 0; i < 5; i++) {
arr[i] = (function () {
return i;
}());
}
arr.forEach(function (el) {
console.log(el); // 0 / 1 / 2 / 3 / 4
});
좀 더 실용적인 예제를 살펴봅시다.
목록을 클릭하여 목록에 해당하는 숫자가 콘솔에 출력되도록 하는 예제를 만들겠습니다.
<ul class="click">
<li>CLICK 1</li>
<li>CLICK 2</li>
<li>CLICK 3</li>
<li>CLICK 4</li>
</ul>
var items = document.querySelectorAll('.click li');
for (var i = 0; i < items.length; i++) {
items[i].onclick = function () {
console.log('click' + i); // 'click4'
}
}
콘솔에는 'click4'
만 출력됩니다.onclick
에 할당되는 핸들러(함수)가 실행되는 시점과, 변수 i
를 참조하는 시점이 다르기 때문에 정상적으로 숫자가 출력되지 않습니다.
그렇다면 잘 동작할 수 있도록 아래와 같이 변경합니다.
var items = document.querySelectorAll('.click li');
for (var i = 0; i < items.length; i++) {
items[i].onclick = (function (j) {
return function () {
console.log('click' + j); // 'click0' / 'click1' / 'click2' / 'click3'
}
}(i))
}
핸들러를 반환하는 즉시실행함수(외부함수)를 생성하고, 지역변수(매개변수)를 만들어 핸들러가 값을 참조 가능하도록 합니다.
이제 핸들러는 반복문의 i
를 참조하지 않고 외부함수의 매개변수인 j
를 참조합니다.
콘솔을 확인하면 순서대로 숫자가 잘 출력됩니다.
ES2015(ES6)의 let
키워드를 사용하면 좀 더 쉽게 구현할 수 있습니다.
var items = document.querySelectorAll('.click li');
for (let i = 0; i < items.length; i++) {
items[i].onclick = function () {
console.log('click' + i); // 'click0' / 'click1' / 'click2' / 'click3'
}
}
자바스크립트에서 일반적인 객체지향 프로그래밍 방식으로 prototype
를 사용하는데 객체 안에서 사용할 속성을 생성할 때 this
키워드를 사용하게 됩니다.
아래 예제에서 속성 this._name
은 _
를 앞에 붙여줌으로 네이밍 컨벤션을 기준으로 외부 접근 불가(Private)의 의미를 가집니다.
그러나 자바스크립트에서는 private
, public
, protected
같은 ‘접근 수정자’를 제공하지 않기 때문에, 예제에서의 속성 _name
은 ‘접근 불가’라는 의미만 가질 뿐 실제론 외부에서 접근이 가능합니다.
따라서 a._name
으로 콘솔에 출력하면 값을 확인할 수 있습니다.
function Hello(name) {
this._name = name;
}
Hello.prototype = {
getName: function () {
return this._name;
},
setName: function (name) {
this._name = name;
}
}
var a = new Hello('good');
console.log(a._name); // 'good'
console.log(a.getName()); // 'good'
a.setName('nice');
console.log(a._name); // 'nice'
console.log(a.getName()); // 'nice'
이제 외부에서 접근할 수 없도록 ‘캡슐화(Encapsulation)’ 해봅시다.
즉시실행함수를 활용합니다.
즉시실행함수 내부에 별도의 객체를 만들고 객체 자체를 반환하는 형태로 Hello
생성자를 만들어 줍니다.
이제 ‘은닉화(Hiding)’된 _name
과 name
은 외부에서 접근할 수 없습니다.
var Hello = (function () {
var _name;
function _Hello(name) {
_name = name;
}
_Hello.prototype = {
getName: function () {
return _name;
},
setName: function (name) {
_name = name;
}
}
return _Hello;
}());
var a = new Hello('good');
console.log(a._name); // undefined
console.log(a.getName()); // 'good'
a.setName('nice');
console.log(a._name); // undefined
console.log(a.getName()); // 'nice'
같은 예제라고 할 수는 없지만 좀 더 쉬운 이해를 위해서 위의 코드를 아래와 같이 단순화할 수 있습니다.
function hello(name) {
var _name = name;
return {
getName: function () {
return _name;
},
setName: function (name) {
_name = name;
}
};
}
아래 예제의 경우 this.name
키워드가 외부 함수의 this
가 아닌 글로벌 this
를 참조하고 있습니다.
비상식적으로 동작하지만 실제로 많은 부분에서 발생하는 문제입니다.
window.name = 'window';
var object = {
name: 'object',
getName: function () {
return (function () {
return this.name;
}());
}
}
console.log(object.getName()); // 'window'
이럴 경우 this
가 참조 가능한 별도의 변수 _this
를 생성할 수 있습니다.
window.name = 'window';
var object = {
name: 'object',
getName: function () {
var _this = this;
return (function () {
return _this.name;
}());
}
}
console.log(object.getName()); // 'object'