그래서 클로저란 무엇인가?
클로저는 함수와 그 함수가 선언됐을 때의 렉시컬 환경(Lexical Environment)과의 조합이다.
- MDN
🤔 ...?
그래서 이게 뭔데...
function outerFunction() {
var x = 10;
var innerFunction = function() {
console.log(x);
}
return innerFunction;
}
var inner = outerFunction();
inner(); // 10
실행 컨텍스트에 대해 공부했다면 이 코드를 보고 한 가지 의문이 들 수 있다.
마지막 줄 inner()
, 즉, innerFunction을 호출할 때에는 outerFunction 함수는 실행 컨텍스트에서 제거된 후 일 것인데, 어떻게 outerFunction 내부의 변수인 x에 접근이 가능할까?
실행 컨텍스트를 이미지화 하면 이렇게 될 것이다.
함수 outerFunction을 호출하면 내부 함수 innerFunction이 반환되고, 함수 outerFunction의 실행 컨텍스트는 소멸한다. 자신을 포함하고 있는 외부 함수보다 내부 함수가 더 오래 유지되는 경우, 외부 함수 밖에서 내부 함수가 호출되더라도 외부함수의 지역변수에 접근할 수 있다. 이미 실행이 끝난 함수(실행 컨텍스트가 사라진)가 아직 살이있는 함수의 스코프 체인에 의해 변수객체가 유지되어 접근이 가능한 상황이라면, 아직 살아있는 함수(콜 스택에 실행 컨텍스트가 올라가 있는 함수)를 클로저(Closure)라고 부른다.
렉시컬 환경은 알지만 렉시컬 스코프는 뭘까?
렉시컬 환경과 렉시컬 스코프의 차이점에 대해서도 알아보았다.
렉시컬 환경은 실행 컨텍스트(Execution Context)에서 변수, 함수 선언 및 다른 식별자에 대한 정보를 담고 있는 데이터 구조이다. 각 실행 컨텍스트는 자체적으로 렉시컬 환경을 가진다.
렉시컬 스코프는 변수 및 식별자의 유효 범위를 나타내는 개념이다. 함수가 어디에서 정의되었는지에 따라 해당 함수의 렉시컬 스코프가 결정된다. 이를 렉시컬 스코핑이라 한다. 렉시컬 스코프는 함수가 정의된 시점의 변수 및 스코프 상태를 기반으로 하며, 함수가 호출될 때마다 이 스코프에서 변수를 검색한다. 결국 렉시컬 스코프는 함수가 선언이 되는 위치에 따라서 상위 스코프가 결정되는 스코프이다.
function outerFunction() {
let x = 'hello';
function innerFunction() {
console.log(x);
}
innerFunc();
}
outerFunction();
내부 innerFunction
이 호출되면 자신의 실행 컨텍스트가 실행 컨텍스트 스택에 쌓이고 변수 객체(Variable Object)와 스코프 체인(Scope Chain) 그리고 this에 바인딩할 객체가 결정된다.
이때, 스코프 체인은 전역 객체와 함수 outerFunction
의 스코프를 가리키는 함수 outerFunction의
활성 객체(변수 객체) 그리고 함수 자신의 스코프를 가리키는 활성 객체를 순차적으로 바인딩한다. 스코프 체인이 바인딩한 객체가 바로 렉시컬 스코프의 실체이다.
내부 함수 innerFunction
가 자신을 포함하고 있는 외부함수 outerFunction
의 변수 x에 접근할 수 있는 것, 다시 말해 상위 스코프에 접근할 수 있는 것은 렉시컬 스코프의 레퍼런스를 차례대로 저장하고 있는 실행 컨텍스트의 스코프 체인을 자바스크립트 엔진이 검색하였기에 가능한 것이다.
실행 컨텍스트는 물리적으로 객체의 형태를 가지며, 3가지의 프로퍼티를 가지고 있다.
- 변수 객체 (Variable Object)
- 스코프 체인 (Scope Chain)
- this value
실행 컨텍스트가 생성되면, 자바스크립트 엔진은 이 실행 컨텍스트에 필요한 정보를 담을 객체를 생성하는데 이를 변수 객체라고 한다.
실행 컨텍스트에 따라 가리키는 객체가 다르다.
[[Scope]]
로 구성되어 있다.Scope = Activation Object + [[Scope]]
[[Scopes]]
프로퍼티를 갖고 있다.[[Scopes]]
프로퍼티는 함수 객체의 내부 프로퍼티로 함수 객체가 실행되는 환경을 가리킨다.[[Scopes]]
프로퍼티는 자신의 실행 환경 Lexical Environment와 자신을 포함하는 외부 함수의 실행 환경인 AO와 전역 객체(Global Object)를 가리키게 된다.[[Scopes]]
는 외부 함수의 실행 환경을 여전히 참조할 수 있다. 이를 클로저라고 한다.[[Scopes]]
VS 스코프 체인
[[Scopes]]
는 함수 객체의 프로퍼티이며, 함수 생성단계에서 결정된다.다시 처음의 정의로 돌아가보자.
클로저는 함수와 그 함수가 선언됐을 때의 렉시컬 환경(Lexical Environment)과의 조합이다.
-MDN-
위 정의에서 말하는 '함수'란 반환된 내부 함수를 의미하고, '그 함수가 선언될 때의 렉시컬 환경 (Lexical Environment)'이란 내부 함수가 선언됐을 때의 스코프를 의미한다. 즉, 클로저는 반환된 내부 함수가 자신이 선언됐을 때의 환경(Lexical Environment)인 스코프를 기억하며, 자신이 선언 됐을 때의 환경(Scope) 밖에서 호출되어도 그 환경(Scope)에 접근할 수 있는 함수를 말한다. 클로저는 자신이 생성될 때의 환경 (Lexical Environment)를 기억하는 함수다.
실행 컨텍스트의 관점에서 설명하면, 내부 함수가 유효한 상태에서 외부 함수가 종료하여 외부 함수의 실행 컨텍스트가 반환되어도 실행 컨텍스트 내의 활성 객체(Activation Object)는 내부 함수에 의해 참조되는 한 유효하여 내부 함수가 스코프 체인을 통해 참조할 수 있는 것을 의미한다.
var box = document.querySelector('.box');
var toggleBtn = document.querySelector('.toggle');
var toggle = (function() {
var isShow = false;
return function() {
box.style.display = isShow ? 'block' : 'none';
isShow = !isShow
}
})()
toggleBtn.onClick = toggle;
isShow
를 기억하는 클로저다.isShow
는 소멸하지 않는다. .box
요소의 표시 상태를 나타내는 변수 isShow
값이 변경된다. 변수 isShow
는 클로저에 의해 참조되고 있기 때문에 유효하며 자신의 변경된 최신 상태를 계속해서 유지한다.이처럼 클로저는 현재 상태를 기억하고 상태가 변경되어도 최신 상태를 유지해야 하는 상황에 유용하다. 만약 클로저가 없다면 상태를 기억하고 변경하기 위해 전역 변수를 사용할 수 밖에 없다.
var increaseBtn = document.getElementById('increase');
var count = document.getElementById('count');
var increase = (function() {
var counter = 0;
return function() {
return ++counter;
};
})();
increaseBtn.onClick = function() {
count.innerHTML = increase();
};
function makeCounter(predicate){
var counter = 0;
return function() {
counter = predicate(counter);
return counter;
}
function increase(n) {
return ++n;
}
function decrease(n) {
return --n;
}
// increaser 함수와 decreaser 함수는 독립된 렉시컬 환경을 갖기 때문에 카운터 상태가 연동되지 않는다.
const increaser = makeCounter(increase);
console.log(increaser()); // 1
console.log(increaser()); // 2
const decreaser = makecounter(decrease);
console.log(decreaser()); // -1
console.log(decreaser()); // -2
}
function Counter() {
var counter = 0;
this.increase = function() {
return ++counter;
};
this.decrease = function(){
return --counter;
};
const counter = new Counter();
console.log(counter.increase()); // 1
console.log(counter.decrease()); // 0
}
var arr = [];
for (var i = 0; i < 5; i++) {
arr[i] = function() {
return i;
};
for (var j = 0; j < arr.length; j++) {
console.log(arr[j])());
}
}
var arr = [];
for (var i = 0; i < 5; i++) {
arr[i] = (function(id) {
return function() {
return id;
};
})(i);
}
for (var j = 0; j < arr.length; j++) {
console.log(arr[j])());
}
위 예제는 자바스크립트 함수 레벨 스코프의 특성으로 인해 for 루프의 초기문에서 사용된 변수의 스코프가 전역이 되기 때문에 발생하는 현상이다. ES6의 let 키워드를 사용하면 해결할 수 있다.
const arr = [];
for (let i = 0; i < 5; i++) {
arr[i] = function() {
return i;
};
};
for(let i = 0; i < arr.length; i++) {
console.log(arr[i]());
};
고차 함수를 이용한 방법
const arr = new Array(5).fill();
arr.forEach((value, index, array) => array[index] = () => index);
arr.forEach(f => console.log(f()));
https://poiemaweb.com/js-closure
https://blacklobster.tistory.com/7
https://yoo11052.tistory.com/154
https://velog.io/@yejineee/실행-컨텍스트의-세-가지-객체-VO-SC-this
개발자로서 배울 점이 많은 글이었습니다. 감사합니다.