[JS] 클로저
2023-02-24
렉시컬 스코프와 함수 객체의 내부 슬롯 Environment

클로저에서 핵심 키워드는 “함수가 선언된 렉시컬 환경” 이다.

const x = 1; function outerFunc() { const x = 10; function innerFunc(){ console.log(x); } innerFunc(); } outerFunc();

outerFunc 함수 내부에서 중첩 함수 innerFunc가 정의되고 호출되었다.

이 때 중첩 함수 innerFunc의 상위 스코프는 외부 함수 outerFunc의 스코프이다.

그렇기 때문에 innerFunc의 x는 상위 스코프인 outFunc안에서 정의된 x를 가리킬 수 있는 것이다.

만약 innerFunc가 outerFunc 외부에서 정의되었다면 x는 전역변수 x를 가리킬 것이다.


이는 자바스크립트가 렉시컬 스코프를 따르기 때문이다.

함수가 정의되고 평가될 때 외부 렉시컬 환경에 대한 참조에 저장할 참조값, 즉 상위 스코프에 대한 참조가 결정된다.


앞에서 실행 컨텍스트를 배울 때, 렉시컬 환경이 생길 때마다, 외부 렉시컬 환경을 가리키는 참조를 걸어줬었다.

함수는 자기가 어디서 호출되는지랑 관계없이 정의된 위치에 따라 상위 스코프를 기억해야 하기 때문에

이를 위해 함수는 자신의 내부 슬롯 Environment 를 사용한다.


foo 가 정의되어 평가될 때는 아직 foo가 실행되기 전이니까 실행 컨텍스트 스택에 foo는 없음

전역 실행 컨텍스트가 가장 상위 실행 컨텍스트, 즉 실행 중인 컨텍스트이다.

이게 바로 foo 함수 객체의 내부슬롯 Environment 에 전역 렉시컬 환경을 저장하는것이다.


코드로 설명하자면

const x = 1; // foo가 정의되어 평가될 때는 전역 실행 컨텍스트가 실행중임. 즉, foo의 객체의 Environment 내부슬롯에는 // 전역 렉시컬 환경이 저장되어 있다. function foo (){ const x = 10; bar(); } // bar도 마찬가지임 function bar() { console.log(x); }

함수가 호출되어야

함수 실행 컨텍스트가 생성되고

함수 렉시컬 환경이 생성되고

함수 환경 레코드가 생성되고

this 바인딩 결정되고

외부 렉시컬 환경에 대한 참조가 결정된다.

맨 마지막에 외부 렉시컬 환경에 대한 참조는 Environment에 저장된애랑 똑같은애이다.

즉 해당 함수의 바로 상위 스코프이다.


클로저와 렉시컬 환경
const x = 1; function outer() { const x = 10; const inner = function () { console.log(x); }; return inner; } const innerFunc = outer(); // outer 가 종료되었으므로, 실행 컨텍스트 스택에서 제거됐을거고, // 그러면 outer안의 지역변수 x는 유효하지 않을 것이다. // 근데 어떻게 여전히 10을 출력할까? innerFunc(); // 10

위 코드처럼


위에서 inner 함수가 평가될 때, 얘는 outer내에서 정의되었기 때문에

inner함수 객체의 Environment 내부 슬롯에는 outer 함수의 렉시컬 환경을 저장하고 있을것이다.

그리고 이것은 함수가 존재하는 한 유지된다.


그리고 outer가 평가될 때는 outer 객체의 내부슬롯 Environment 에는 전역 렉시컬 환경을 저장하고 있을것이다.


outer가 호출되면, 얘의 렉시컬 환경이 생성되고 앞서 outer 함수 객체의 내부슬롯 Environment에 저장된 전역 렉시컬 환경을

outer 함수 렉시컬 환경의 “외부 렉시컬 환경에 대한 참조” 에 할당할 것이다.


그리고 inner가 평가될때는 (근데 inner는 함수 표현식으로 정의해서 런타임에 평가됨)

inner는 자기 Environment 내부슬롯에 outer 함수의 렉시컬 환경을 저장할것이고

그리고 outer가 종료되면 inner 반환하면서 outer함수는 끝나며 실행 컨텍스트 스택에서 제거된다.

근데 잊지마라 inner는 아직 실행되지 않았다,,


근데 앞에서 배운거처럼 outer 실행 컨텍스트가 제거되었다고 해서 렉시컬 환경이 바로 소멸되지 않는다.

언제까지? 아무도 참조안할때까지. 근데 누가 참조하냐? inner의 Environment가 참조한다.

그럼 inner는 누가 참조하냐? 전역변수 innerFunc가 참조한다.

그럼 가비지 컬렉터가 없애지 못 할 것이고그럼 사라지지 않게 되는 것이다.


inner를 호출하면 inner 함수의 실행 컨텍스트가 생성되고 스택에 푸쉬된다.

그리고 얘의 렉시컬 환경의 외부 렉시컬 환경에 대한 참조에 그 Environment에 있는 애 들어가게 된다.


순서 다시 파악하자면

함수 정의, 평가 ⇒ 함수 객체 생기고 environment 에 상위 스코프 저장

함수 호출 ⇒ 실행 컨텍스트 생성, 스택에 푸쉬, 렉시컬 환경 생기고, this바인딩 결정되고, 외부렉시컬 참조까지


암튼 이렇게 되어서 inner가 outer보다 더 오래 살아 남게 되었다. (실행 컨텍스트 상에서)

그러나 inner는 inner의 상위스코프는 여전히 outer임을 기억하고 있다.

따라서 inner 내부에서는 상위 스코프를 참조할 수 있고, 식별자를 참조할수 있고, 식별자의 값을 바꿀수도 있다.


자바스크립트의 모든 함수는 상위 스코프를 기억하기 때문에 이론적으로 모든 함수는 클로저이다.


하지만, 일반적으로는 모든 함수를 클로저라고 하진 않는다.

상위 스코프의 식별자를 참조할 때 클로저라고한다.

만약 상위 스코프의

참조하지도 않는 식별자를 기억하는 것은


const x = 1; function outer() { const x = 10; function inner() { console.log(x); }; inner(); } outer();

위 코드에서 inner는 상위 스코프인 outer의 x를 참조한다. 따라서, inner는 클로저이다.

그러나, outer는 inner를 반환하지 않기 때문에, inner는 outer보다 먼저 종료된다.

즉, 생명 주기가 먼저 종료된 외부함수의 식별자를 참조한다는 클로저의 본질에 적합하지 않다.

따라서, 이런 경우도 inner를 클로저라 하지 않는다.

이때, 클로저에 의해 참조되는 상위 스코프의 변수를

자유 변수에 묶여 있는 함수이므로, 클로저라고 하는 것이다.


클로저의 메모리 점유는 브라우저가 알아서 최적화하므로 걱정하지 않아도 된다.


클로저의 활용

클로저는 자바스크립트의 유용한 기능이므로 잘 활용하면 좋다.

클로저는 상태를 안전하게 변경하고, 유지하기 위해 사용한다.

상태가 의도치 않게 변경되지 않도록 안전하게 은닉하고, 특정 함수에게만 상태 변경을 허용하기 위해 사용한다.

이 때, 특정 함수를 클로저로 쓰면 된다는 것이다.


호출된 횟수가 안전하게 변경되고 유지해야할 상태라고 가정하자

let num = 0; const increase = function () { return ++num; }; console.log(increase()); // 1 console.log(increase()); // 2 console.log(increase()); // 3

위 코드는 안전하지 않다. num의 값이 increase가 호출되기 전에 바뀌지 않아야 한다.

즉, num은 increase만이 변경할 수 있어야 하기 때문이다.

하지만 카운트 상태인 num은 전역변수로 관리하고 있어 누구나 접근할 수있기 때문이다.


increase함수만이 num을 참조하도록 변경해보자.

const increase = function(){ let num = 0; return ++num; } console.log(increase()); // 1 console.log(increase()); // 1 console.log(increase()); // 1

위 코드도 올바르지 않다.

increase가 호출될때마다 num이 0으로 초기화되기 때문이다.


클로저를 활용해보자.

const increase = (function() { let num = 0; return function(){ return ++num; } }()); console.log(increase()); // 1 console.log(increase()); // 2 console.log(increase()); // 3

위 코드가 실행되면 즉시 실행 함수가 호출된다.

그리고 즉시 실행 함수가 반환하는 함수가 increase 변수에 할당된다.

increase에 할당된 이 함수는 정의된 위치에 의해 상위 스코프인 즉시 실행 함수의 렉시컬 환경을 기억하는 클로저다.

즉시 실행 함수는 호출 즉시 소멸되지만, 얘가 반환한 이 클로저는 increase 변수에 할당되어 호출된다.

이 클로저는 상위 스코프의 num을 기억하고 있다.

num은 외부에서 접근할 수 없는 지역 변수이므로 안전하다.


이렇게 외부함수가 소멸되고, 중첩함수가 이를 안전하게 은닉해 값을 참조할 수 있을 때

중첩함수가 클로저로서 빛을 발한다는 것을 알 수 있다.


const Counter = (function () { let num = 0; function Counter() {} Counter.prototype.increase = function () { return ++num; }; Counter.prototype.decrease = function () { return --num; }; return Counter; })(); const counter = new Counter(); console.log(counter.increase()); // 1 console.log(counter.increase()); // 2 console.log(counter.decrease()); // 1 console.log(counter.decrease()); // 0

생성자 함수를 활용할 수도 있다.

num은 생성자 함수 Counter랑 무관한 즉시실행 함수에서 정의된 변수이다.

따라서 생성자 함수 Counter가 만든 인스턴스에서 마음대로 num을 참조할 수 없다.

그러나 increase와 decrease는 함수 정의가 평가되어 함수 객체가 될 때

상위 스코프인 즉시 실행 함수의 렉시컬 환경을 기억하는 클로저이다.

즉, 즉시 실행 함수가 종료되어 소멸되어도 increase와 decrease는 num을 참조할 수 있다.

num은 increase와 decrease에 의해 은닉되어 안전하게 변경될 수 있어 클로저를 잘 활용하고 있는 예이다.