Search

클로저

24.1 렉시컬 스코프

어디서 실행이아닌 어디서 선언했는지가 중요합니다. 렉시컬 환경의 외부 렉시컬 환경에 대한 참조 에 저장할 참조값, 즉 사우이 스코프에 대한 참조는 함수 정의가 평가되는 시점에서 함수가 정의된 환경에 의해 결정됩니다.

24.2 함수 객체의 내부 슬롯 [[Environment]]

함수는 자신의 내부 슬롯에 자신이 정의된 환경 즉 사우이 스코프의 참조를 저장합니다. 이는 자신이 호출되었을 때 생성될 함수 렉시컬 환경의 외부 렉시컬 환경에 대한 참조에 저장될 참조값입니다.
const x = 1; function foo(){ const x = 10; bar(); } function bar(){ console.log(x); } foo(); // ? bar(); // ?
JavaScript
복사
foo 함수와 bar 함수는 전역에서 함수 선언문으로 정의되었습니다. 둘다 전역 코드가 평가되는 시점에 평가되어 함수 객체를 생성하고 전역 객체 window의 메서드가 됩니다. 이때 생성된 함수 객체의 내부 슬롯에는 함수 정의가 평가된 시점 즉 코드 평가 시점에 실행 중인 컨텍스트의 렉시컬 환경이 저장됩니다.
함수 코드 평가시 아래와 같은 순서로 진행됩니다.
1.
함수 실행 컨텍스트 생성
2.
함수 렉시컬 환경 생성
a.
함수 환경 레코드 생성
b.
this 바인딩
c.
외부 렉시컬 환경에 대한 참조 결정
이때 함수 렉시컬 환경의 구성요소인 외부 렉시컬 환경에 대한 참조에는 함수 객체의 내부 슬롯 [[Environment]]에 저장된 렉시컬 환경의 참조가 할당됩니다.

24.3 클로저와 렉시컬 환경

const x = 1; function outer(){ const x = 10; const inner = function () { console.log(x) };\ return inner; } const innerFunc = outer(); innerFunc();
JavaScript
복사
외부 함수보다 중첩 함수가 더 오래 유지되는 경우 중첩 함수는 이미 생명 중기가 종료한 외부 함수의 변수를 참조할 수 있는데 이러한 중첩함수를 클로저라 합니다.
outer 함수의 실행이 종료하면 innter 함수를 반환하면서 outer 함수의 생명주기가 종료됩니다. 즉 outer 함수의 실행 컨텍스트가 실행 컨텍스트 스택에서 제거됩니다. 하지만 렉시컬 환경까지 소멸하는 것은 아닙니다. innterFunc에서 참조하고 있으므로 가비지 컬렉션의 대상이 되지 않기 때문입니다.
JS의 모든 함수는 사우이 스코프를 기억해 이론적으론 클로저지만 상위 스코프의 어떤 식별자도 참조하지 않는다면 클로저가 아닙니다.
또한 비록 외부함수를 참조하고 있더라도 외부함수보다 중첩 함수의 생명주기가 더 짧다면 클로저가 아닙니다.
클로저에 의해 참조되는 상위 스코프의 변수를 자유 변수라고 부릅니다.

24.4 클로저의 활용

클로저는 상태를 안전하게 변경하고 유지하기 위해 사용합니다. 상태를 은닉하고 특정함수에 상태변경을 의존할 수 있기 때문입니다.
const increase = (function() { let num = 0; return funtion(){ return ++num; } }()); console.log(increase()); // 1 console.log(increase()); // 2 console.log(increase()); // 3
JavaScript
복사
코드 실행 시 즉시 실행함수가 호출되고 즉시 실행 함수가 반환한 함수가 increase 변수 에 할당됩니다. 이때 즉시 실행 함수는 소멸되지만 클로저는 increase 변수에 할당되어 호출됩니다. 이때 상위 렉시컬 환경을 기억하고 있기 때문에 num 변수가 초기화될 일이 없습니다.
또한 increase 는 어디서 실행되든 상관없습니다. 즉 prototype등에서 실행되더라도 결과는 같습니다.
또한 함수를 호출해 반환된 함수의 경우 각자 자신만의 독립된 렉시컬 환경을 갖습니다.

24.5 캡슐화와 정보 은닉

캡슐화랑 객체의 상태를 나타내는 프로퍼티와 이를 참조하는 동작인 메서드를 하나로 묶은 것을 이야기합니다. 이를 감추는 것을 정보 은닉이라고합니다. 이를 통해 객체간의 상호 의존성 즉 결합도를 낮출 수 있습니다.
function Person(name, age){ this.name = name; let _age = age; this.sayHi = function(){ console.log(${this.name} ${_age}); }; } const me = new Person('Lee', 20); me.sayHi(); console.log(me.name); consle.log(me._age); const you = new Person('Kim', 30); you.sayHi(); console.log(you.name); console.log(you._age);
JavaScript
복사
만약 sayHi 메서드는 인스턴스 메서드이므로 Person 객체가 생성될 때마다 중복 생성됩니다. 이를 방지위한 코드는 프로토타입 메서드로 변경하는 방법입니다.
function Person(name, age){ this.name = name; let _age = age; } Person.prototype.sayHi = function(){ console.log(${this.name} ${_age}); };
JavaScript
복사
그러나 이렇게 되었을 때는 지역변수들을 참조하지 못합니다. 따라서 하나로 모아줘야합니다.
function Person = (function(){ let _age = 0; function Person(name, age){ this.name = name; let _age = age; Person.prototype.sayHi = function(){ console.log(${this.name} ${_age}); }; return Person; }()); const me = new Person('Lee', 20); me.sayHi(); console.log(me.name); consle.log(me._age); const you = new Person('Kim', 30); you.sayHi(); console.log(you.name); console.log(you._age);
JavaScript
복사
이렇게되면 JS에서도 정보 은닉이 가능한 것처럼 보입니다. 하지만 위에서도 _age의 변수 상태가 유지되지 않는다는 문제가 있습니다.

24.6 자주 발생하는 실수

var funcs = []; for(var i = 0; i < 3; i++){ funcs[i] = function() { return i; }; } for (var j = 0; j < funcs.length; j++){ console.log(funcs[j]()); }
JavaScript
복사
위의 코드의 결과값은 0 1 2 가 나올거라 생각할 수 있지만, 실제로는 3이 출력됩니다.
var는 블록 레벨 스코프가아닌 함수레벨 스코프이기 때문입니다.
var funcs = []; for(var i = 0; i < 3; i++){ funcs[i] = (function (id) { return function() { return id; }; }(i)); } for(var j = 0; j < funcs.length; j++){ console.log(funcs[j]()); }
JavaScript
복사
이는 var에서 기인한 문제이기 때문에 let, const로 변경한다면 이렇게 됩니다.
const funcs = []; for(let i = 0; i < 3; i++){ funcs[i] = function () { return i; }; } for(let i = 0; i < funcs.length; i++){ console.log(funcs[i]()); }
JavaScript
복사
이렇게 된다면 for문이 실행 될때마다 새로운 코드블록의 렉시컬 확경이 생성됩니다.
또 다른 방법으로 고차 함수를 이용한 방법이 있습니다.
const funcs = Array.from(new Array(3), (_, i) => i); funcs.forEach(f => console.log(f()));
JavaScript
복사