모던 자바스크립트 딥다이브 교재로 이해하고
MDN 까지 한 번 슈우우욱 훑고나서 정리하는 내용이다.
이전 파트의 실행 컨텍스트와 렉시컬 환경에 대해서 잘 이해했는지 , 어려운 개념인 것 같음에도 불구하고
이해가 좀 빠륵게 되었다.
클로저는 함수와 그 함수가 선언된 렉시컬 환경과의 조합이다.
MDN
에서 클로저에 대해 정의한 내용이다.
어떤 식으로 작동하는건지 코드를 통해 먼저 살펴보자
function MakeCounter() {
let num = 0;
return {
increase() {
num += 1;
},
decrease() {
num -= 1;
},
value() {
return num;
},
};
}
const counter = MakeCounter();
console.log(counter.value()); // 0
counter.increase();
counter.increase();
console.log(counter.value()); // 2
counter.decrease();
console.log(counter.value()); // 1
console.log(counter.num); // undefined
console.log(num); // ReferenceError: num is not defined
생긴 코드를 살펴보면 MakeCounter
라는 함수 안에 num
이란 지역 변수를 만들어 두고 {increase, decrease, value}
함수들이 담긴 객체를 반환한다.
해당 객체를 counter
라는 식별자에 담아 메소드인 increase , decrease
들을 몇 번 실행하고 value
를 통해 값을 확인해보니 값이 사용한 메소드에 맞춰 num
값이 바뀐 것을 볼 수 있다.
그럼 num
값이 어디에 생겼나 ? 보려고 하니
counter
객체에도 존재하지않고 전역에도 존재하지 않고 있다.
이처럼 클로저
를 이용하면 외부에서 참조 할 수 없는 프로퍼티를 다룰 수 있는 함수를 말한다.
이처럼 클로저는 모듈 패턴
과 깊은 연관이 있다.
모듈 패턴
모듈 패턴(Module Pattern)은 소프트웨어 디자인 패턴 중 하나로, JavaScript에서 코드를 모듈화하고 캡슐화하기 위한 방법을 제공합니다. 이 패턴은 코드의 유지보수성을 향상시키고 전역 스코프의 오염을 방지하는 데 도움이 됩니다.
모듈 패턴의 주요 특징은 다음과 같습니다:
캡슐화 (Encapsulation): 모듈 패턴은 변수와 함수 등을 하나의 단일 단위로 묶어 캡슐화합니다. 이로써 모듈 내부의 세부 사항을 감추고 외부에서 직접 접근할 수 없도록 보호할 수 있습니다.
네임스페이스 (Namespace): 모듈 패턴은 전역 네임스페이스를 사용하여 변수나 함수의 충돌을 방지합니다. 모듈 내부에서 선언된 변수와 함수는 해당 모듈의 스코프 내에서만 유효하므로 다른 모듈과 충돌할 위험이 줄어듭니다.
재사용성 (Reusability): 모듈은 독립적으로 개발되고 테스트되기 때문에 재사용성이 높아집니다. 다른 프로젝트에서 필요한 모듈을 가져와 사용하거나, 현재 프로젝트에서 여러 곳에서 동일한 모듈을 사용할 수 있습니다.
유지보수성 (Maintainability): 코드를 모듈 단위로 나누면 각 모듈을 독립적으로 관리할 수 있어 유지보수가 용이해집니다. 특정 모듈만 업데이트하거나 수정할 수 있어 전체 코드베이스에 영향을 덜 주게 됩니다.
모든 소스 코드는 소스 코드 별로 실행 컨텍스트
가 존재하며 실행 컨텍스트
는 렉시컬 환경
에 맞춰 관리 된다고 하였다.
전역 소스 코드는 전역 렉시컬 환경을 생성하고 함수 소스 코드는 함수 렉시컬 환경을 생성한다.
이 때 각 실행 컨텍스트는 선언되는 상황에 맞춰 실행 컨텍스트와 렉시컬 환경이 생성되고
선언된 컨텍스트로부터 상위 컨텍스트를 연결하는 스코프 체인
으로 이뤄져있다고 했다.
렉시컬 환경
변수와 함수의 식별자를 기억하고 관리하는 역할을 한다.
각 실행 컨텍스트는 자신만의 렉시컬 환경을 가지며, 이는 해당 컨텍스트의 식별자와 메소드들에 대한 정보를 담고 있다.
스코프 체인
스코프 체인은 각 렉시컬 환경에서 상위 렉시컬 환경으로 연결된 것을 말한다. 이는 변수나 함수를 찾을 때 사용되는 메커니즘이며, 현재 렉시컬 환경에서 찾지 못한 경우 상위 렉시컬 환경으로 이동하여 검색을 계속한다.
간단히 코드를 통해 이해해보자
const x = 1; // 전역 렉시컬 환경에 존재하는 식별자
function foo() {
const x = 10; // foo 렉시컬 환경에 존재하는 식별자
function bar() {
console.log(x);
}
bar();
}
foo(); // 10
console.log(x); // 1
foo
내부에서 선언된 bar
는 bar의 렉시컬 환경
을 가진다. 이 때 bar
내부에는 x
라는 프로퍼티가 없기 때문에 선언된 위치의 상위 렉시컬 환경인 foo의 렉시컬 환경
으로 이동하여 프로퍼티 x : 10
를 찾아 console.log
한다.
전역에서 x
를 찾으려 하면 전역 렉시컬 환경
에 존재하는 프로퍼티 x : 1
을 찾아 로그한다.
이처럼 함수
들은 선언 될 때 본인만의 렉시컬 환경
을 가지며, 선언된 위치를 본인의 상위 렉시컬 환경
으로서 스코프 체인을 연결한다.
[[Environment]]
객체들은 다양한 내부 프로퍼티와 내부 메소드를 갖는다고 하였다.
함수들도 일급 객체이기 때문에 내부 프로퍼티, 내부 메소드를 갖는데
상위 렉시컬 환경에 대한 내용을 [[Environment]]
에 담는다.
즉 위 예시에서 전역 공간에서 선언된 foo
함수의 [[Environment]]
는 전역 객체인 window or global
이며 foo의 렉시컬 공간
에서 선언된 bar
함수의 [[Environemt]]
는 foo의 렉시컬 공간
이다.
이전에 함수의 렉시컬 환경에 존재하는 객체인 FunctionEnvironment
에는 함수의 매개 변수 및 argument , 지역 변수 들이 선언된다고 하였고 OuterLexicalEnvironmentRefrence
에는 상위 렉시컬 환경에 존재하는 객체들 (상위 객체의 FunctionEnvironemtn , OuterLexicalEnvironmentRefeence
) 등이 담긴다고 하였다.
결국 OuterLexicalEnvironmentRefrence
은 내부 프로퍼티인 [[Environment]]
가 가리키고 있는 렉시컬 환경의 객체들을 참조한다.
챗 지피티는
OuterLexicalEnvironmentRefeence
라는 객체명이 자바스크립트 명세서에서 정확하게 쓰는 용어가 아니라고 한다.
내가 공부하고 있는모던 자바스크립트 딥다이브
교재에선느 해당 객체로 설명을 이어나가기 때문에 나는 사용하도록 하겠다.
중요한 점은 함수는 본인이 선언된 위치를 상위 렉시컬 환경으로[[Environment]]
에 지정하고 본인만의 렉시컬 환경도 구성한다는 점이다.
function outer() {
let num = 0;
function increaseInner() {
num += 1;
console.log(num);
}
return increaseInner;
}
const increaser = outer();
console.log(increaser);
increaser(); // 1
increaser(); // 2
increaser(); // 3
해당 코드를 살펴보면 outer
라는 함수 안에 increaseInner
가 선언되어 있고 선언된 해당 함수가 반환된다.
그렇다면 outer
와 increaseInner
의 [[Environment]] (상위 렉시컬 환경)
는 무엇을 가리키고 있을까 ?
Function | [[Environment]] |
---|---|
outer | Global Lexical Environment |
increaseInner | outer Lexical Environment |
반환되는 increasInner
함수는 outer
을 상위 렉시컬 환경으로 가리키고 있는 함수 객체이다.
const increaser = outer()
으로 increaseInner
를 할당하는 것인데
할당 이후 outer
함수의 생명 주기는 끝남에도 불구하고 outer
내부에서 선언된 increaseInner
함수의 생명주기는 끝나지 않고 사용 가능하다.
또한 outer
의 생명주기가 끝났음에도 불구하고 increaseInner
함수는 outer
내부에서 정의된 num
에 대해서 접근이 가능하다.
outer
의 생명주기가 끝났음에도 불구하고increaseInner
함수가 상위 렉시컬 환경으로outer
를 가리키고 있기 때문에outer
내에 존재하는 지역 변수에 접근 가능하다.
이처럼 중첩함수(increaseInner
)가 외부함수(outer
) 보다 생명 주기가 길고, 외부 함수의 생명주기가 종료되었음에도 불구하고 참조 가능한 함수를 클로저
라고 한다.
이제 위에서 설명한
클로저는 함수와 그 함수가 선언한 렉시컬 환경과의 조합이다.
라는 문구가 좀 이해가 된다.
그러면 클로저 함수 왜 사용해야 할까 ?
let num = 0;
function Counter() {
return {
increase() {
num += 1;
},
decrease() {
num -= 1;
},
value() {
console.log(num);
},
};
}
const counter = Counter();
counter.increase();
counter.increase();
counter.value(); // 2
num = 9999;
counter.value(); // 9999
만약 전역 변수에 존재하는 num
의 값을 참조하여 변경하는 Counter
함수가 존재 할 때
전역 변수인 num
은 값의 재할당이 일어날 수 있다.
하지만 클로저를 사용하면 외부에서 직접 접근하지 못하게 하고, 변수의 상태를 함수 내부에서만 조작, 외부에서는 읽기 전용으로 가능하게 할 수가 있다.
function Counter() {
let num = 0;
return {
increase() {
num += 1;
},
decrease() {
num -= 1;
},
value() {
console.log(num);
},
};
}
const counter = Counter();
counter.increase();
counter.increase();
counter.value(); // 2
console.log(num); // ReferenceError: num is not defined
클로저를 사용하면 함수를 동적으로 생성하는 함수 팩토리를 만들 수 있다.
이를 통해 비슷한 기능을 하는 함수를 여러 개 만들 수 있고, 각 함수는 자신만의 렉시컬 스코프를 가지게 된다.
function MakeCounter(aux) {
let num = 0;
return function () {
num = aux(num);
return num;
};
}
function increas(x) {
x += 1;
return x;
}
function decrease(x) {
x -= 1;
return x;
}
const increaser = MakeCounter(increas);
const decreaser = MakeCounter(decrease);
console.log(increaser()); // 1
console.log(increaser()); // 2
console.log(decreaser()); // -1
console.log(decreaser()); // -2
자바스크립트에서 함수는 일급 객체이기 때문에 다른 함수의 인수로 넣을 수 있다고 했다.
그렇기 때문에
function MakeCounter(aux) {
let num = 0;
return function () {
num = aux(num);
return num;
};
}
해당 함수에서 aux
인수로 함수를 받고, MakeCounter
렉시컬 환경 내 지역 변수인 num
의 값을 aux
로 값을 계속 변환시킨 후 변환 시킨 num
을 반환하는 함수를 만들었다.
그렇다면 MakeCounter(increase)
의 경우 반환되는 함수 객체는 다음처럼 생겼다.
function () {
num = increase(num);
return num;
}
해당 함수를 실행 시킬 때 마다 num
값을 1증가 시킨후 반환하게 되는데 이 때 가져오는 num
값은 선언 할 때 설정한 MakeCounter(aux)
내부에 존재하는 let num =0
을 가져오게 된다.
값이 변환되면 MakeCounter(aux)
내부의 num
값도 1씩 증가하게 될 것이다.
여기서 포인트는 MakeCounter(increase)
와 MakeCounter(decrease)
의 렉시컬 스코프가 독립적으로 작동한다는 것인데
이는 [[Environment]]
에 들어가는 상위 렉시컬 환경은 할당 때마다 생성되기 때문이다.
const increaser = MakeCounter(increas); // increaser 가 참조하고 있는 상위 렉시컬 환경 1 생성
const decreaser = MakeCounter(decrease); // decreaser 가 참조하고 있는 상위 렉시컬 환경 2 생성
좀 더 자세히 살펴보자
함수가 정의 될 때 함수 내부에서 사용하는 변수들은 함수가 정의된 스코프에 대한 참조를 갖는다.
함수가 실행 될 때가 아닌, 정의 될 때의 스코프를 기억한다.
function MakeCounter(aux) {
let num = 0;
return function () {
num = aux(num);
return num;
};
}
MakeCounter(aux)
내에서 정의된 let num = 0;
은 MakeCounter
함수 내부 ~ 하위 객체에서만 접근이 가능하다.
클로저는 함수와 함수가 정의된 렉시컬 환경의 조합이다.
함수가 다른 함수 내부에서 정의되면 내부 함수는 외부 함수의 렉시컬 환경을 기억하며 , 외부 함수의 렉시컬 환경을 상위 렉시컬 환경
이라고 한다.
function MakeCounter(aux) {
let num = 0;
return function () {
num = aux(num);
return num;
};
}
에서 return function() {...}
에 정의된 함수는 상위 렉시컬 환경으로 MakeCounter(aux)
를 가지며, MakeCounter(aux)
에서 정의된 num
에 접근 가능하다.
const increaser = MakeCounter(increas); // increaser 가 참조하고 있는 상위 렉시컬 환경 1 생성
const decreaser = MakeCounter(decrease); // decreaser 가 참조하고 있는 상위 렉시컬 환경 2 생성
에서 매 호출마다 새로운 렉시컬 환경을 생성한다.
렉시컬 스코프 환경을 정의하는 것과 렉시컬 환경을 생성하는 것은 다르다.
increaser
가 따르고 있는 렉시컬 환경을decreaer
나 전역에서 조회하는 것은 불가능하다.
각 함수가 동작 할 때 마다 ex : increaser()
increaser
는 선언 때 생성된 렉시컬 환경
내에서 동작한다.
서로 자신만의 렉시컬 환경을 가지고 있기 때문에 상태 변화가 독립적으로 이뤄진다.
렉시컬 환경은 독립적이지만 렉시컬 환경의 스코프 체인의 형태는 동일하다.
클로저를 사용하면 비동기 작업에서 현재 상태를 유지하면서 이후에도 해당 상태를 이용할 수 있다.
이는 주로 콜백 함수에서 상태를 유지하고 활용하는데 유용하다.
function asyncOperation() {
let result;
setTimeout(function () { // 클로저
result = "Operation completed";
console.log(result);
}, 1000);
console.log("Operation started");
}
asyncOperation();
해당 비동기 작업을 통해 result
변수가 1초 후에 작업이 완료되었다고 외부 값을 변경해주는 코드이다.
중첩 함수로 클로저를 사용하였으며, 클로저를 이용하여 외부 렉시컬 환경의 변수의 최신 상태를 유지하고 활용 할 수 있다.
이처럼 클로저는 상태가 의도치않게 변경되지 않도록 안전하게 은닉하고 특정 함수에게만 상태 변경을 허용하여 상태를 안전하게 변경하고 유지하기 위해 사용한다.
위 파트에서 렉시컬 환경의 구조는 동일해도, 생성 될 때 마다 독립적인 렉시컬 환경을 갖는다고 하였다.
그렇다면 독립적이지 않고 서로 참조 가능하게 하려면 어떻게 해야 할까 ?
위 카운터 예시에서 increase, decrease
함수가 같은 렉시컬 환경에서 num
을 참조하게 하고 싶다.
그러면 렉시컬 환경을 하나만 만들면 된다.
function MakeCounter() {
let num = 0;
return {
increase() {
num += 1;
},
decrease() {
num -= 1;
},
value() {
console.log(num);
},
};
}
const counter = new MakeCounter();
counter.increase();
counter.increase();
counter.value(); // 2
counter.decrease();
counter.value(); // 1
구우웃 ~
new
를 이용하여 새로운 객체라고 명확히 명시해주는 이유는 가독성 뿐이 아니라this
를 명확히 바인딩 하기 위함도 있다.
캡슐화
는 프로퍼티나 메소드등을 하나의 객체로 묶는 것을 의미하고
정보 은닉
은 특정 프로퍼티나 메소드를 감추기 위해 캡슐화
를 이용하는 것을 말한다.
대부분의 객체 지향 프로그래밍 언어에서는 클래스를 정의하고 그 안에서 public , private , protected
등의 접근제한자를 이용하여 공개 범위를 한정 할 수 있지만
자바스크립트에는 해당 기능이 없다.
하지만 클로저를 이용하여 비슷하게 구현 할 수는 있다.
function Person(name, age) {
this.name = name; // 정보 공개
const _age = age;
this.introduce = function introduce() {
console.log(`hi i am ${this.name} and ${_age} years old`);
};
}
let tom = new Person('tom', 20);
tom.introduce(); // hi i am tom and 20 years old
console.log(tom.name); // tom
console.log(tom.age); // undefined
console.log(tom._age); // undefined
Person
함수 내부에 _age
라는 지역 변수에 인수 age
를 집어 넣어줬다.
이후 Person
내에 정의된 메소드인 introduce
는 상위 렉시컬 스코프로 Person
에 접근 할 수 있기 때문에 _age
에는 접근 가능하기에 해당 메소드는 실행 가능하다.
하지만 _age
나 age
는 tom
의 프로퍼티가 아닐 뿐더러 new Person
이 아닌 Person
에서만 접근 가능한 프로퍼티이다.
그럼 this.introduce
를 프로토타입으로 돌릴 수 있을까 ?
function Person(name, age) {
this.name = name; // 정보 공개
const _age = age;
}
Person.prototype.introduce = function introduce() {
console.log(`hi i am ${this.name} and ${_age} years old`);
};
let tom = new Person('tom', 20);
tom.introduce(); // ReferenceError: _age is not defined
참조 에러가 발생한다.
tom.introduce
는 사실 tom.prototype.introduce
에 해당하는데 결국 Person._age
에 접근하려고 하는 것이다.
이 때 _age
는 Person
안에서 선언된 지역 변수일 뿐, Person
의 프로퍼티가 아니라 참조 에러가 발생한다.
나는 곧죽어도 프로퍼티로 넣고 싶어 어떻게 해
ㅋㅋ 즉시 실행 함수 쓰세요
const MakePerson = (function IIFE() {
let _age = 0;
function Person(name, age) {
this.name = name;
_age = age;
}
Person.prototype.introduce = function introduce() {
console.log(`hi i am ${this.name} and ${_age} years old`);
};
return Person;
})();
let tom = new MakePerson('tom', 20);
tom.introduce(); // hi i am tom and 20 years old
let jerry = new MakePerson('jerry', 40);
jerry.introduce(); // hi i am jerry and 40 years old
으아악 이 코드 이해하는데 한시간이나 걸렸다.
우선 해당 즉시 실행 함수를 이용하면 왜 prototype
이 가능해지는걸까 ?
(function IIFE() {...})()
부분을 통째로 이해해보자
해당 함수가 실행되면 즉시 실행 함수 내부에 _age
라는 지역 변수가 우선 생성된다.
그리고 함수 안에서 Person
함수가 name , age
라는 매개변수를 받아 name
은 Person
으로 인해 생성될 객체의 name
프로퍼티로 설정된다.
age
값은 IIFE()
함수 내부에 존재하는 _age
의 값을 변경 시킨다.
이후 Person.prototype.introduce
를 설정해주는데 이 때 this.name
을 통해 이름은 찾을 수 있지만 _age
는 Person
으로 인해 만들어진 객체 내부에서는 찾을 수 없다.
하지만 Person
은 IIFE
를 상위 렉시컬 환경으로 가리키고 있기 때문에 _age
를 찾아 프로토타입으로 설정해줄 수 있다.
이전에는 안됐던게 되는 이유
function Person(name, age) { this.name = name; // 정보 공개 const _age = age; } Person.prototype.introduce = function introduce() { console.log(`hi i am ${this.name} and ${_age} years old`); }; let tom = new Person('tom', 20); tom.introduce(); // ReferenceError: _age is not defined
이전에 했던 것에선
Person
의_age
를 프로퍼티로 설정해주지 않고 지역변수로만 설정하여 은닉해줬다.
하지만 지역 변수로만 설정해주었기 때문에Person
이 생성하는 객체에서 찾을 수 없었다.
하지만IIFE
로 한 번 더 검싸주어 상위 렉시컬 환경을 생성해준 후 상위 렉시컬 환경에서_age
를 지역 변수로 사용하기 때문에 찾을 수 있었다.
그리고 Person
함수를 반환한다.
Person
함수는 IIFE
함수 내부에 존재하는 클로저 함수인 것이다.
오케이 ~~ const MakePerson = (function IIFE() {...}
까지는 이해했다.
그럼 실행은 어떻게 되는지 보자
let tom = new MakePerson('tom', 20);
tom.introduce(); // hi i am tom and 20 years old
이 부분에서 new MakePerson('tom', 20)
은 사실 new Person('tom', 20)
과 같다.
이 때의 person
은 IIFE()
를 상위 렉시컬로 가리키고 있는 Person
이다.
new Person('tom', 20)
이 실행되면서
function Person(name, age) {
this.name = name;
_age = age;
}
Person.prototype.introduce = function introduce() {
console.log(`hi i am ${this.name} and ${_age} years old`);
};
이런 프로퍼티를 갖고 있는 객체를 tom
에 할당해주었는데
할당해주는 동안 Person
함수 내부의 _age = age
가 작동되며 IIFE
에 존재하는 _age
의 값도 들어간 인수인 20
으로 변경된다.
그 다음 !
let jerry = new MakePerson('jerry', 40);
jerry.introduce(); // hi i am jerry and 40 years old
를 실행하면 let jerry = new MakePerson('jerry', 40);
이부분 역시
let jerry = new Person('jerry', 40);
과 같으며 현재 호출된 Person
함수 역시 이전의 IIFE
를 상위 렉시컬 환경으로 가지는 클로저이다.
이 때
IIFE
의_age
값은 0이 아닌 이전에 설정한 20이다.
그 이유는 클로저의 외부 함수는 실행 될 때 마다 독립적인 렉시컬 환경을 생성하지만 즉시 실행 함수는 단 한번만 실행 되었기 때문에 같은 렉시컬 환경을 공유 한다.
이것 또한 Person
함수 내부의 _age = age
가 작동되며 IIFE
내부에 존재하는 _age
의 값이 이전에 설정한 20
에서 40
으로 변경된다.
값이 변경되며 jerry
의 introduce
는 잘 설정되었지만 이후에 tom
을 설정하면
tom.introduce(); // hi i am tom and 40 years old
tom
이 참조하고 있는 IIFE
의 _age
값이 재할당되었기 때문에 값이 변경된다.
정리
위에서
즉시 실행 함수
를 이용하여 캡슐화와 은닉을 시행하였지만 즉시 실행 함수는 단 한번만 사용되어 렉시컬 환경을 하나만 생성한다.
렉시컬 환경을 생성하여, 생성자 함수 외부에 지역 변수를 생성하여 은닉한 채로 프로토타입 설정이 가능하였다.
하지만 렉시컬 환경을 하나만 공유하고, 인스턴스들이 같은 렉시컬 환경을 공유하여 지역 변수의 값이 변경된다는 단점이 있었다. (바뀐 값을 다른 인스턴스들도 공유)
개빡치쥬 ?
그런데 최근 버전에선
private 필드 정의 제안
이 가능하다고 한다.
좀 더 진도 나가고 배워보자
너무 어려워서 지피티랑 더블체크 했다.
너만 믿는다
var funcs = [];
for (var i = 0; i < 3; i += 1) {
funcs[i] = function () {
return i;
};
}
for (var j = 0; j < funcs.length; j += 1) {
console.log(funcs[j]());
} // ??
코드를 보기만 해도 불편해진다.
반복문에서 함수 레벨 스코프를 갖는 var
를 사용하다니
하지만 코드를 실행하면 0 , 1 , 2
가 나올 것만 같다.
하지만 이 출력값은 3 ,3 ,3
이 나온다.
왜 ?
var funcs = [];
for (var i = 0; i < 3; i += 1) {
funcs[i] = function () {
return i;
};
}
가 실행 될 때 funcs
배열에 들어가는 것은 i
값이 차례로 0 , 1 , 2
인 원시값이 담길 것 같지만 그것이 아닌 식별자 i
가 들어간다.
이 때 식별자 i
는 전역 변수로서 반복문이 실행됨에 따라 값이 재할당 된다.
funcs[i]
안에 들어있는 변수들은 모두 재할당 된 i
를 return
하게 되어있기 때문에 각 배열은 모두 최종적으로 재할당 된 3
을 출력하게 된다.
var funcs = [];
for (var i = 0; i < 3; i += 1) {
funcs[i] = (function (id) {
return function () {
return id;
};
})(i);
}
console.log(funcs[0]()); // 0
console.log(funcs[1]()); // 1
console.log(funcs[2]()); // 2
즉시 실행 함수를 이용하여 넣어주었다.
i
값이 들어올 때 id
라는 매개변수를 받아 즉시 function(){return id}
값을 반환하는
즉시 실행 함수를 시행 시켜주었다.
funcs
배열에 들어간 함수들은 function(){return id}
인데 반복문이 실행 될 때 마다 즉시 실행 함수가 실행되어 funcs
에 들어간 함수들은 모두 동일하게 생긴 `function(){return id}
이지만 각자의 상위 렉시컬 환경이 다르다.
첫 번째 function(){return id}
는 i
가 0일때 시행된 즉시 실행 함수를 렉시컬 환경으로 가리키고 두 번째는 i 가 1일 때 시행된 즉시 실행 함수를 렉시컬 환경으로 갖는다.
이처럼 전역 변수를 다루는 경우엔 렉시컬 환경을 다르게 만든 후 클로저를 이용하여 구할 수 있다.
이런 문제를 해결하기 위해선 렉시컬 환경을 새롭게 구성
해야 한다.
방금은 즉시 실행 함수를 이용하여 골치아프게 해줬는데
반복문에서 렉시컬 환경을 새롭게 구성하는 방법은 매우 쉽다.
var funcs = [];
for (let i = 0; i < 3; i += 1) {
funcs[i] = function () {
return i;
};
}
console.log(funcs[0]()); // 0
console.log(funcs[1]()); // 1
console.log(funcs[2]()); // 2
그건 바로 블록 레벨 스코프를 갖는 let
을 이용해주는 것이다.
반복문이 시행 될 때 마다 function() {return i}
가 가리키고 있는 상위 렉시컬 환경이 생성되기 때문에 쉽게 해결된다.
let 최고 키킥