모던 자바스크립트 Deep Dive 글 목록(스터디)
https://hello-kk.tistory.com/780
클로저를 이해하기 전에, 짧게 영상으로 주요 내용 보고 정리
https://hello-kk.tistory.com/779
클로저
: 함수와 그 함수가 선언된 렉시컬 환경의 조합
실행 컨텍스트 단원을 하고 와서 그런지 대략적으로 이해는 할 수 있지만 위의 '클로저' 정의는 딱 와닿지 않았다. 구체적인 예시도 떠오르지 않은 상태.
렉시컬 환경의 조합? 외부 렉시컬 환경에 대한 참조를 말하는 건가?
위의 문장에서 이해 해야 할 부분은 3가지로 보인다.
1. 함수와
2. 그 함수가 선언된 렉시컬 환경의
3. 조합
"만약 innerFunc 함수가 outerFunc 함수의 내부에서 정의된 중첩 함수가 아니라면, innerFunc 함수를 outerFunc 함수의 내부에서 호출한다 하더라도 outerFunc 함수의 변수에 접근할 수 없다."
=> '중첩 함수'라는것과, '외부 함수의 변수에 접근하는 것'이 힌트가 될 것 같다.
렉시컬 스코프
: 상위 스코프에 대한 참조는 함수 정의가 평가되는 시점에, 함수가 정의된 환경에 의해 결정된다.
상위 스코프
: 함수 객체의 내부 슬롯[[Environment]]에 저장된 현재 실행중인 실행 컨텍스트의 렉시컬 환경의 참조
선언문으로 정의된 함수의 경우,
상위 스코프의 코드가 평가되는 시점에 (선언문으로 정의된)함수가 평가되어, 함수 객체를 생성한다.
함수가 호출되면 함수 내부로 코드의 제어권이 이동, 함수 코드를 평가
외부 렉시컬 환경에 대한 참조에, 함수 객체의 내부 슬롯 [[Environment]]에 저장된 렉시컬 환경의 참조가 할당
=> 즉, 이전에 함수 객체를 생성했던 때 내부 슬롯[[Environment]] 에 렉시컬 환경 참조
(실행 컨텍스트는 소멸됐지만, 실행 컨텍스트가 참조했던 그 환경만 객체로 살아있다 ?)
outer 함수의 실행이 종료되면 outer 함수의 실행 컨텍스트는 실행 컨텍스트 스택에서 제거된다.
outer 함수의 실행 컨텍스트가 제거되었기 때문에 outer 함수의 지역변수 x에 접근할 수 있나?라는 의문을 제기함.
(클로저에 대한 힌트가 될 것 같다.)
innerFunc를 호출하면, 이미 실행 주기가 종료되어 실행 컨텍스트 스택에서 제거된 outer 함수의, 지역변수 x를 불러올 수 있다.
외부 함수보다 중첩 함수가 더 오래 유지되는 경우, 중첩 함수는 이미 생명 주기가 종료한 외부 함수의 변수를 참조할 수 있다. 이러한 중첩 함수를 클로저라고 한다.
중첩 함수가 더 오래 유지되는 경우?
1. 외부 함수에서 중첩 함수를 return
2.
위의 '이해 해야할 부분 3가지'를 되짚어 보았다.
1. 함수와
2. 그 함수가 선언된 렉시컬 환경=>함수가 정의된 위치의 스코프. 즉,상위 스코프를 의미하는 실행 컨텍스트의 렉시컬 환경
3. 조합
상위 스코프를 의미하는 실행 컨텍스트의 렉시컬 환경과의 조합이라는 의미는,
(1) outer 함수가 선언문으로 정의되어 있는 경우
전역 코드를 평가할 때 outer 객체가 생성된다고 했다. 그러면, outer 객체에는 [[Envirionment]] 내부 슬롯이 있고 거기에 상위 스코프로 전역 렉시컬 환경을 저장한다.
(2) outer 함수가 호출된 경우
outer 함수의 렉시컬 환경이 생성된다.
그리고 (1)에서의 outer 함수 객체의 [[Environment]] 내부 슬롯에 저장된 전역 렉시컬 환경을 outer 함수 렉시컬 환경의 '외부 렉시컬 환경에 대한 참조'에 할당한다.
*inner 함수의 경우, 표현식으로 작성되었기 때문에 런타임에 평가된다고 한다. (p394) (그러면 내부 중첩으로 작성된 함수 선언문이 있다면, 그건 ..런타임때 평가되는게 아닌건가?, )
- 중첩함수 inner는 자신의 [[Environment]] 내부 슬롯에, 현재 실행중인 실행 컨텍스트의 렉시컬 환경을 상위 스코프로 저장
렉시컬 환경의 구조도를 보고싶어서, 개발자 도구의 "Console" 과 "Source"를 사용해볼 수 있지 않을까 싶었는데..
https://ko.javascript.info/debugging-chrome
계속해서 상위 스코프의 렉시컬 환경과 식별자에 관한 설명이 나온다.
outer 함수의 실행 컨텍스트는 실행 컨텍스트 스택에서 제거되지만, outer 함수의 렉시컬 환경이 소멸되는 건 아니다.
=> outer 함수의 렉시컬 환경은, inner 함수의 [[Environment]] 내부 슬롯에 의해 참조되고 => inner 함수는 전역 변수인 innerFunc에 의해 참조되고 있다.
∴가비지 컬렉션의 대상이 되지 않는다.
중첩 함수인 inner는 상위 스코프를 참조할 수 있기 때문에, 상위 스코프의 식별자를 참조할 수 있고 식별자의 값을 변경할 수도 있다.
클로저라고 하지 않는 경우
1. 단순히 중첩 함수이면 안된다.
책에서 예시로 든 것과 같이 상위 스코프의 식별자를 참조하지 않는다면 클로저가 아니다.
2. 상위 스코프의 식별자를 참조했지만(=클로저 였지만) 곧바로 소멸한 경우는 클로저가 아니다. (외부로 함수가 반환되지 않았다. 외부 함수보다 중첩 함수의 생명주기가 짧았다.)
여기까지 읽었을 때, 정리한 클로저란
=> 상위 스코프의 식별자를 참조하는 중첩 함수이며, 외부 함수의 외부로 반환되어 외부 함수보다 더 오래 살아 남는 함수
(상위 스코프의 식별자를 참조하지 않는다면, 상위 스코프를 기억하지 않는다.)
자유 변수: 클로저에 의해 참조되는 상위 스코프의 변수
클로저: 자유 변수에 묶여있는 함수
클로저가 상위 스코프의 식별자를 참조하는게 없다면, 참조하지 않는 식별자는 외부 함수의 생명주기가 종료했을 때 같이 제거된다고 한다: 자바스크립트 엔진의 최적화
클로저를 사용하는 이유가 뭘까?
상태를 안전하게 변경하고 유지하기 위해 사용.=> 특정 함수에게만 상태 변경을 허용하기 위해 사용
★이전에 14장 전역 변수의 문제점에서, '즉시 실행 함수', '모듈화'와 같이 생각해볼 수 있을 것 같은데 다시 찾아보기!
클로저를 사용하더라도, 참조하는 식별자가 전역 변수라면 암묵적 결합으로 의도되지 않은 결과를 초래할 수도 있다.
클로저를 사용한다면 참조하고 있는 식별자의 상태를 유지할 수가 있다.(중첩함수인 클로저를 몇번을 호출하더라도 재차 초기화되지 않는다. 중첩 함수가 의도한대로 동작할 뿐!)
여기까지 이해한 바!
클로저가 되려면
1. 중첩 함수여야 하고,
2. 외부 함수의 식별자 중에는 중첩 함수가 참조하는 식별자가 있어야 한다.
3. 그리고! 중첩 함수가 외부 함수에 의해 반환되어야 한다.(외부 함수보다 생명주기가 길어야 한다)
여기까지 보면 변수(식별자)를 바꾸는건 중첩함수가 하는데.. 그러면 변수를 호출하는 건? 그 변수를 어디서 쓸 수 있지?
만약 중첩 함수가 병렬적으로 반환된다면, 객체를 반환한다. 이때 객체는 메서드를 갖는다.
(함수를 하나 반환하면 함수였지만, 함수를 여러개 반환해서 객체로 넘어오면 사용 방법이 클래스와 비슷한 느낌)
const counter = (function(){
let num = 0;
return {
increase(){
return ++num;
},
decrease(){
return --num;
}
}
}())
console.log(counter.increase())
console.log(counter.decrease())
책의 406 페이지에서 콜백 함수를 사용하는데, 콜백 함수를 매개변수와 반환 부분을 화살표 함수랑 비교하니까 신선했다.(비교하는 부분은 책에선 없다)
여기서, 즉시 실행 함수를 호출하는 부분을 똑같이 다시 호출한다면 (당연히?) 새로운 렉시컬 환경을 갖는다.
그런데, 즉시실행 함수와 똑같은 기능을 하는 일반 함수를 만든다고 하더라도
해당 함수를 호출해서 다른 식별자에 할당할 때마다 새로운 렉시컬 환경이 만들어진다. (실행 컨텍스트가 만들어지는 과정부터 ~)
즉, const counter에서의 num과
const counter_later 에서의 num은 변수명은 똑같더라도 독립적으로 동작한다는 의미.
클래스에서 생성자를 떠올려보았는데,
const student_1 = new Student()
const student_2 = new Student()
student_1, student_2은 각각 독립된 프로퍼티를 갖는 것과 클로저가 새로운 렉시컬 환경을 구성하는게 비슷하다고 생각한다.
다음에 나오는 내용도 자연스러운 과정인 것 같다. 중첩 함수가 식별자를 변화시키는 과정을 안전하게 하기위해서니까.
캡슐화: 객체 상태를 나타내는 프로퍼티와 메서드를 하나로 묶는 것.
정보 은닉: 정보 보호, 결합도(객체간 상호 의존성)를 낮춘다
24.6 자주 발생하는 실수
(어렵다. 어딘가 명확하게 이해하지 못한 부분이 있는 것 같다.)
const 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]())
}
놀라운걸. . . ? 진짜인지 출력해봤다.
일단, 잘못 이해한 부분
1. i는 전역변수니까.. 그럼 최종적으로 3만 다 들어가는 건가? 라고 생각했다.
=> 3이 들어가는게 아니라 i가 return 된다는 점에 주목해야 하는 것 같다.
결국 funcs[i]는 함수를 return 받는다. function(){return i} 함수는 클로저랑 비슷하다(중첩이고 외부 함수의 i를 참조하니까. 그런데 생명주기 관점에서는.. 모르겠다)
funcs[i]는 결국 전역변수인 i와 관련있다.
funcs[0]은 function() {return i; }
funcs[1]은 function() {return i; }
funcs[2]는 function() {return i; }
모두 저렇게 구성되어 있다.
이해가 안되어서 var i로 선언된 식별자 i의 값을 임의로 바꿔주었더니, funcs[j] 출력 결과도 다음처럼 변했다.
아직 좀 불명확하게 느껴진다.. 왜지? i 를 return.. 하는건데 for문에서 i는 0,1,2 였는데..
i의 값을 리턴하는게 아니라, i라는 변수를 리턴하는 의미인 것 같은데..
와닿지는 않았다.
var funcs = [];
for (var i=0; i<3; i+=1){
funcs[i] = (function (id){
return function(){
return id;
}
}(i))
}
for(var j=0; j<funcs.length;j+=1){
console.log(funcs[j]())
}
즉시 실행 함수가 반환한 함수는 funcs 배열에 순차적으로 저장된다고 한다.
그리고 즉시 실행 함수가 반환한 중첩 함수(여기서 return id하는 함수)는 자신의 상위 스코프를 기억하는 클로저이고 id는 즉시 실행 함수가 반환한 중첩 함수에 묶인 자유 변수가 되어 그 값이 유지된다고 한다.
동일한 코드에서 let i 를 사용하면 이런 문제가 발생하지 않는다.
for문이 반복 실행될 때마다 for문 코드 블록의 새로운 렉시컬 환경이 생성된다.
(블록의 경우 let const가 있어야만 렉시컬 환경이 생기나? var는 렉시컬 환경이 생기지 않는건가? 싶어서
선언부가 없는 블록 만들어서 질문해봤는데, 의미없는.. 블록이라고 했다....
GPT의 답변으로는,
만약 코드 블록 안에 변수나 함수가 선언되어 있다면, 그 선언에 따라 새로운 렉시컬 환경이 생성됩니다.
=> 즉, var 또는 let/const 로 선언된 경우에만 렉시컬 환경이 생성되는 것 같다. 어쩌면 당연한 건가.. 선언이 없다면 블록을 쓸 이유가.. 없을 것 같으니까..)
(또 이해가 불분명한 지점이 왔다)
for문에서 let i =0 을 한 경우, 식별자 i는 값을 유지해야 한다(0 →1 →2로 바뀌어야 하기 때문에)
유지해야 하기 때문에 독립적인 렉시컬 환경을 생성해서 식별자의 값을 유지한다고 한다.
순서대로 적어보았다.
1. for문의 변수 선언문에서 let 키워드로 선언한 초기화 변수를 사용한 for문이 평가된다
2. 새로운 렉시컬 환경을 생성
3. 식별자와 값을 등록한다.
4. 새롭게 생성된 렉시컬 환경을 현재 실행중인 실행 컨텍스트의 렉시컬 환경으로 교체한다 (실행 컨텍스트 스택 최상단에 올린다는 것을 의미하는 건가?)
let, const 키워드 반복문은 코드 블록을 반복 실행할 때마다 새로운 렉시컬 환경을 생성하고 스냅샷을 찍는 것처럼 저장.
'Front-End > JavaScript' 카테고리의 다른 글
[헷갈린 내용 정리] 19장 프로토타입 (모던 자바스크립트 Deep Dive) (0) | 2023.04.10 |
---|---|
19장 프로토타입 (모던 자바스크립트 Deep Dive) (0) | 2023.04.10 |
모던 자바스크립트 Deep Dive(스터디) (0) | 2023.04.05 |
클로저(Closure) (0) | 2023.04.05 |
13. 스코프 14. 전역 변수의 문제점 15. let, const 키워드와 블록 레벨 스코프 (모던 자바스크립트 Deep Dive) (0) | 2023.04.03 |