● 클로저
- 클로저란 '함수와 그 함수가 선언된 렉시컬 환경과의 조합이다.' 사실 정의만보면 이해하기에 난해한 개념이라고 한다.. ^^ 사실 맞는 것 같다. 하지만 여기서 중요한 건 함수가 선언된 렉시컬 환경이다.
const x = 1;
function outerFunc(){
const x = 10;
function innerFunc(){
console.log(x)
}
innerFunc();
}
outerFunc();
위의 예시를 보면, 중첩함수인 innerFunc의 상위스코프가 outerFunc이므로, innerFunc가 호출되었을 때, 외부함수인 outerFunc의 변수에 접근할 수 있다.
만약 innerFunc가 outerFunc의 내부에서 정의된 중첩함수가 아니라 innerFunc를 outerFunc 내부에서 호출한다고 하더라도 outerFunc의 변수에 접근할 수 없다.
const x = 1;
function outerFunc(){
const x = 10;
innerFunc();
}
function innerFunc(){
console.log(x)
}
outerFunc();
// 1
자바스크립트 엔진은 함수를 어디서 호출했는지가 아니라 함수를 어디에 정의했는지에 따라 상위스코프를 결정한다. 이를 렉시컬 스코프(정적 스코프)라고한다.
const x = 1;
function foo(){
const x = 10;
bar();
}
function bar(){
console.log(x);
}
foo();
bar()
위의 예제에서 foo함수와 bar함수는 전역에서 정의된 함수이다. 함수의 상위스코프는 함수를 어디서 정의했느냐에 따라 정의되므로, foo함수와 bar함수의 상위스코프는 전역이다. 함수를 어디서 호출하는지는 함수의 상위 스코프 결정에 어떠한 영향도 주지 못한다. 함수의 상의 스코프는 함수를 정의한 위치에 의해 정적으로 결정되고 변하지 않는다.
스코프의 실체는 실행 컨텍스트의 렉시컬 환경이다. 이 렉시컬 환경은 자신의 '외부렉시컬 환경에 대한 참조'를 통해 상위 렉시컬 환경과 연결된다. 이것이 바로 스코프체인이다.
렉시컬 환경의 "외부 렉시컬 환경에 대한 참조"에 저장할 참조값, 즉 상위 스코프에 대한 참조는 함수 정의가 평가되는 시점에 함수가 정의된 환경에 의해 결정된다. 이것이 바로 렉시컬 스코프이다.
var x = 1;
function func(){
function func2(){
return 1;
}
return 2;
)
위와 같이 전역에서 실행컨텍스트는 전역 실행컨텍스트를 스택에 넣고 소스평가를 시작한다. 전역 렉시컬 환경을 생성하고 전역 환경 레코드 생성 - 객체 환경 레코드 정의된 var x, func 함수를 전역객체의 프로퍼티와 메서드로 등록한다. 이후 외부 렉시컬 환경에 대한 참조 결정을 해야하는데, 이때 외부소스코드의 렉시컬 환경, 상위스코프를 가리킨다. 즉 여기서는 상위스코프가 전역이기 때문에, 렉시컬 환경의 외부 렉시컬 환경에 대한 참조에 null이 할당된다.
즉, 아래의 과정과 같이 보았을 때, 전역 스코프에서 정의된 함수와 변수는 외부 렉시컬 환경에 대한 참고값이 null , 즉 전역을 가리키고 있으므로, 이 함수와 변수는 전역에서 정의되었다는 것을 알 수 있다는 것이다.
근까 걍 쉽게 말하면 참조값보니까 null이네;; 어 이거 변수랑 함수 전역에서 정의된거구나? 하고 알 수 잇다는거임
● 함수 객체의 내부 슬롯 [[Environment]]
- 함수는 자신의 내부 슬롯 [[Environment]]에 자신이 정의된 환경, 즉 상위 스코프의 참조를 저장한다. 다시말해, 함수 정의가 평가되어 함수 객체를 생성할 때 자신이 정의된 환경에 의해 결정된 상위 스코프의 참조를 함수 객체 자신의 내부 슬롯에 저장한다. 이때 자신의 내부 슬롯에 저장된 상위 스코프의 참조는 현재 실행중인 실행 컨텍스트의 렉시컬 환경을 가리킨다.
== 왜냐하면 함수 정의가 평가되어 함수 객체를 생성하는 시점은 함수가 정의된 환경, 즉 상위 함수가 평가 또는 실행되고 있는 시점이며, 이때 현재 실행중인 실행 컨텍스트는 상위 함수의 실행 컨텍스트이기 때문이다.
함수 렉시컬 환경의 구성 요성인 외부 렉시컬 환경에 대한 참조에는 함수 객체의 내부 슬롯 [[Environment]]에 저장된 렉시컬 환경의 참조가 할당된다.
호걱스.. 실행컨텍스트의 과정에서 만들어졌던 외부 렉시컬 환경에 대한 참조가 함수 객체의 내부 슬롯 [[Envrionment]] 를 그대로 할당한 것이었다..!!!
● 클로저
const x = 1;
function outer(){
const x = 10;
const inner = function(){ console.log(x)};
return inner;
}
const innerFunc = outer(); // (1)
innerFunc(); (2)
// 10;
먼저 (1)에서 outer 함수를 호출하면 outer함수는 중첩함수 inner를 반환하고 생명주기를 마감한다. 즉, outer함수의 실행이 종료되면 함수의 실행컨텍스트는 스택에서 제거된다. 이때 outer함수의 지역 변수 x와 변수 값 10을 저장하고 있던 outer 함수의 실행 컨텍스트가 제거되었으므로 outer함수의 지역 변수 x 또한 생명 주기를 마감한다. 따라서 outer함수의 지역 변수 x는 유효하지 않기 때문에 x에 접근할 수 있는 방법은 없어보인다. (스코프 체인 때문에)
그러나 위 코드 실행 결과는 outer 함수의 지역 변수 x의 값인 10이다. 어떻게 이런 일이 발생할 수가 있는 것일까?
이처럼 외부 함수보다 중첩함수가 더 오래 유지되는 경우 중첩 함수는 이미 생명주기가 종료한 외부 함수의 변수를 참조할 수 있다. 이러한 중첩 함수를 클로저라고 부른다. 즉 외부함수의 생명주기보다 더 오래 유지되며 외부함수의 스코프 바깥에서 외부함수의 값을 불러 읽을 수 있다면 클로저로 보는 것이다.
함수는 이전과 같이 말한 것처럼 자신의 상위 스코프를 기억한다. 함수는 어디서 호출하든 상관없이 함수는 언제나 자신이 기억하는 상위 스코프의 식별자를 참조할 수 있으며 식별자에 바인딩된 값을 변경할 수도 있다. [[Environment]]의 내부 슬롯에 저장된 값은 상위 스코프 함수가 존재하는 한 유지된다.
저 예시를 설명해보자
1) outer 함수가 평가되어 함수 객체를 생성할 때, 현재 실행중인 실행컨텍스트의 렉시컬 환경, 즉 전역 렉시컬 환경을 outer함수 객체의 내부 슬롯[[Environment]]에 상위 스코프로서 저장한다.
2) outer 함수를 호출하면, outer 함수의 렉시컬 환경이 생성되고 otuer 함수 객체의 내부 슬롯에 저장된 전역 렉시컬 환경을 outer함수 렉시컬 환경의 "외부 렉시컬 환경에 대한 참조" 에 할당한다.
3) 중첩함수 inner가 평가된다. inner은 자신의 내부 슬롯[[Environment]]에 현재 실행 중인 실행 컨텍스트의 렉시컬 환경, 즉 outer함수의 렉시컬 환경을 상위 스코프로서 저장한다.
4) outer함수의 실행이 종료하면 inner함수를 반환하면서 outer의 생명주기가 종료된다.
5) 즉, outer함수의 실행 컨텍스트가 실행 컨텍스트에서 제거된다. 컨텍스트 스택에서 제거되었지만 렉시컬 환경까지 소멸하는 것은 아니다.
6) inner함수가 호출되면 inner함수의 실행 컨텍스트가 생성되고, inner 렉시컬 환경의 외부 렉시컬 환경에 대한 참조에는 inner 내부슬롯[[Environment]]에 저장된 값이 할당된다.
중첩함수 inner은 outer함수보다 더 오래 생존했다. 이런 배은망덕한!!
이런 중첩함수는 외부함수의 생존여부(실행 컨텍스트의 생존)와 상관없이 자신의 정의된 위치에서 결정된 상위 스코프를 기억한다. inner 내부에서는 상위 스코프를 참조할 수 있고, 상위 스코프의 식별자도 참조할 수 있고, 식별자의 값도 변경할 수 있다.
하지만 외부함수안에 있는 어떤 중첩함수라도 다 클로저가 될 순 없다. 클로저가 되려면 조건이 있다 이말씀
function foo() {
const x = 1;
const y = 2;
function bar() {
const z = 3;
debugger;
console.log(z);
}
return bar;
}
const bar = foo();
bar();
위에서 bar함수는 foo 외부함수의 어떤 식별자도 참조하고 있지 않다. foo함수보다 생명주기는 길지만, 어떤 식별자도 참조하고 있지 않기 때문에 이런 함수는 클로저라고 볼 수 없다.
function foo() {
const x = 1;
function bar() {
debugger;
console.log(x);
}
bar();
}
foo();
위의 예제를 보면, bar함수는 foo 의 식별자인 x를 참조하고 있다. 그럼 이거 클로저임?
ㄴㄴ 잘보셈 bar의 함수 생명주기가 foo보다 짧잖삼. 외부함수보다 일찍 소멸되니까 이거 아님. 클로저 조건이 머엿삼
1) 외부함수보다 생명의 주기가 길고, 2) 외부함수의 식별자를 참조해야한다.
function foo() {
const x = 1;
const y = 2;
function bar() {
debugger;
console.log(x);
}
return bar;
}
const bar = foo();
bar();
위 같은 코드가 클로저라는 거삼. 즉, 외부함수는 늘 내부함수를 리턴해야된다고 봐야될듯? 그래야 전역 or 외부함수의 상위 스코프에서 외부함수 식별자들을 접근할 수 있기 때문임. 클로저의 의의는 그것인듯.. 저의 개인적인 의의정의지만, 클로저의 의의는 원래는 특정 스코프 안에만 있어야지 식별자들을 인식할 수 있는데, 그 스코프 바깥에 있는 얘가, 특정 스코프 안에 있는 식별자를 참조할 수도 있고 식별자를 수정할 수도 있다는 점이 큰 의의같다.
클로저에 의해 참조되는 상의 스코프의 변수는 자유변수라고 부른다. "함수가 자유변수에 대해 닫혀있다" 라는 의미이다. 위의 예시를 보면 const x 가 자유변수라고 볼 수 있다.
● 클로저의 활동
- 데이터를 보존하는 함수
- 만약 유저가 웹 어플리케이션에 들어올 때마다 인사를 입력값을 받고, "안녕, 유저!" 라는 말을 콘솔창에 내보내보자.
function hello(user) {
let greeting = "안녕";
function userName() {
console.log(`${greeting} ${user}!`);
}
return userName;
}
const userName = hello("다은");
userName();
// 안녕 다은!
위같이 들어올때마다 user 라는 값을 보존할 수 있다.
const increase = (function () {
let num = 0;
return function () {
return ++num;
};
}());
console.log(increase()); // 1
console.log(increase()); // 2
console.log(increase()); // 3
코드를 보면 첫번째 외부함수를 즉시 실행하고, 내부함수를 리턴한다. 그다음 콘솔에서 내부함수를 실행하는 것이다.
실행할 때, num이라는 식별자가 외부함수에 있는 식별자이므로 그 식별자에 ++를 해주고 리턴해주는 것이다. 그 식별자를 인식하는 이유는 모두 상위스코프를 기억하고 있기 때문에, 즉 렉시컬 환경을 기억하고 있기 때문에 num을 참조하고 변경할 수 있다.
const counter = (function(){
let num = 0;
return {
increase(){
return ++num;
},
decrease(){
return num > 0 ? --num : 0;
}
};
}());
console.log(counter.increase()); // 1
console.log(counter.increase()); // 2
console.log(counter.decrease()); // 1
console.log(counter.decrease()); // 0
위 예제에서 즉시 실행 함수가 반환하는 객체 리터럴은 즉시 실행 함수의 실행 단계에서 평가되어 객체가 된다. 이때 객체의 메서드도 함수 객체로 생성된다. 객체 리터럴의 중괄호는 코드 블록이 아니므로 별도의 스코프를 생성하지 않는다.
위 예제의 increase, decrease 메서드의 상위 스코프는 increase, decrease 메서드가 평가되는 시점에 실행 중인 실행 컨텍스트인 즉시 실행 함수 실행 컨텍스트의 렉시컬 환경이다. 따라서 increase, decrease 메서드가 언제 호출되든 상관없이 increase, decrease 함수는 즉시 실행 함수의 스코프의 식별자를 참조할 수 있다.
- 모듈 패턴
function makeCalculator() {
let displayValue = 0;
return {
add: function(num) {
displayValue = displayValue + num;
},
subtract: function(num) {
displayValue = displayValue - num;
},
multiply: function(num) {
displayValue = displayValue * num;
},
divide: function(num) {
displayValue = displayValue / num;
},
reset: function() {
displayValue = 0;
},
display: function() {
return displayValue
}
}
}
const cal = makeCalculator();
cal.display(); // 0
cal.add(1);
cal.display(); // 1
console.log(displayValue) // ReferenceError: displayValue is not defined
makeCalculator는 리터럴 객체를 반환한다. 리터럴 객체는 즉시 실행 함수의 실행 단계에서 평가되어 객체가 된다. 객체의 메서드도 함수 객체로 생성된다. 객체 리터럴의 중괄호는 코드 불록이 아니므로 별도의 스코프를 생성하지 않는다. 즉, add, subtract, multiply••• 메서드가 언제 어디서 호출되는 상관없이 실행 함수의 스코프의 식별자를 참조할 수 있다.