Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

24-1장 클로저 - 나세현 #985

Merged
merged 1 commit into from
Dec 12, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 142 additions & 0 deletions docs/24_클로저/나세현.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
## 📔 24장: 클로저

클로저는 함수와 **그 함수가 선언된 렉시컬 환경**과의 조합이다.

```javascript
const x = 1;

function outerFunc() {
const x = 10;

function innerFunc() {
console.log(x); // 10
}

innerFunc();
}

outerFunc();
```

중첩 함수 `innerFunc`의 상위 스코프는 외부 함수 `outerFunc`의 스코프다.
만약 `innerFunc` 함수가 `outerFunc` 함수 내부에서 정의된 중첩 함수가 아니라면, `innerFunc` 함수를 `outerFunc` 내부에서 호출한다고 해도 `outerFunc` 함수의 변수(`x`)에 접근할 수 없다.
이런 현상이 발생하는 이유는 자바스크립트가 **렉시컬 스코프를 따르는** 프로그래밍 언어이기 때문이다.

### ✨ 24.1: 렉시컬 스코프

**자바스크립트 엔진은 함수를 어디서 호출했는지가 아니라 함수를 어디에 정의했는지에 따라 상위 스코프를 결정하며, 이를 렉시컬 스코프(정적 스코프)라고 한다.**
**렉시컬 환경의 "외부 렉시컬 환경에 대한 참조"에 저장할 참조값, 즉 상위 스코프에 대한 참조는 함수 정의가 평가되는 시점에 함수가 정의된 환경(위치)에 의해 결정되며, 이것이 바로 렉시컬 스코프다.**

### ✨ 24.2: 함수 객체의 내부 슬롯 `[[Environment]]`

렉시컬 스코프ㅏ 가능하려면 함수는 자신이 정의된 환경, 즉 상위 스코프를 기억해야 한다.
이를 위해 함수는 **자신의 내부 슬롯 `[[Environment]]`에 상위 스코프의 참조를 저장한다**.
함수 내부에서 정의된 함수 표현식은 외부 함수 코드가 실행되는 시점에 평가되어 함수 객체를 생성하므로, 이때 `[[Environment]]`에는 외부 함수 코드 실행 시점에 실행 중인 실행 컨텍스트의 렉시컬 환경인 외부 함수 렉시컬 환경의 참조가 저장된다.
**함수 객체는 내부 슬롯 `[[Environment]]`에 저장한 상위 스코프를 자신이 존재하는 한 기억한다.**

```javascript
const x = 1;

function foo() {
const x = 10;
bar();
}

function bar() {
console.log(x);
}

foo();
bar();
```

`foo`, `bar` 함수는 모두 전역에서 함수 선언문으로 정의되었기 때문에 전역 코드가 평가되는 시점에 평가되어 함수 객체를 생성하고 전역 객체 `window`의 메서드가 된다.

### ✨ 24.3: 클로저와 렉시컬 환경

```javascript
const x = 1;

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

const innerFunc = outer();
innerFunc();
```

`outer` 함수를 호출하면 이는 중첩 함수 `inner`를 반환하고 생명 주기를 마감하며 동시에 내부에 있던 지역 변수 `x` 또한 생명 주기를 마감한다.
하지만 위 코드의 실행 결과(`innerFunc()` 호출 결과)는 `outer` 함수의 지역 변수 `x`의 값인 `10`이다.
이처럼 **외부 함수보다 중첩 함수가 더 오래 유지되는 경우, 중첩 함수는 이미 생명 주기가 종료한 외부 함수의 변수를 참조할 수 있으며 이런 중첩 함수를 클로저라고 부른다**.
위 예제에서 `outer` 함수가 평가되어 함수 객체를 생성할 때, 전역 렉시컬 환경을 `outer` 함수 객체의 `[[Environment]]` 내부 슬롯에 상위 스코프로서 저장한다.
`outer` 함수가 호출되면 `outer` 함수의 렉시컬 환경이 생성되고, 전역 렉시컬 환경을 `outer` 함수 렉시컬 환경의 "외부 렉시컬 환경에 대한 참조"에 할당한다.
그 다음 중첩 함수 `inner`가 평가되며 (함수 표현식으로 정의했기 때문에 런타임에 평가됨) 이는 자신의 `[[Environment]]` 슬롯에 `outer` 함수의 렉시컬 환경을 상위 스코프로서 저장한다.
`outer` 함수의 실행이 종료하면 `inner` 함수를 반환하면서 `outer` 함수의 생명 주기가 종료된다.
**다시 말해 `outer` 함수의 실행 컨텍스트는 실행 컨텍스트 스택에서 팝되어 제거되지만, 그렇다고 `outer` 함수의 렉시컬 환경까지 소멸하는 것은 아니다.**
`outer` 함수의 렉시컬 환경은 `inner` 함수의 `[[Environment]]` 슬롯에 의해 참조되고 있고 `inner` 함수는 전역 변수 `innerFunc`에 의해 참조되고 있으므로 가비지 컬렉션의 대상이 되지 않기 때문이다.
결과적으로 중첩 함수 `inner`는 외부 함수 `outer`보다 더 오래 생존했으며, 이때 외부 함수보다 더 오래 생존한 중첩 함수는 외부 함수의 생존 여부와 상관없이 자신이 정의된 위치에 의해 결정된 상위 스코프를 기억한다.
이렇게 중첩 함수 `inner`의 내부에서는 상위 스코프를 참조할 수 있으므로 상위 스코프의 식별자를 참조할 수 있고 그 값을 변경할 수도 있다.

자바스크립트의 모든 함수는 상위 스코프를 기억하므로 이론적으로 모든 함수는 클로저이지만, 일반적으로 그렇게 모든 함수를 클로저라고 하지는 않는다.

```javascript
function foo() {
const x = 1;
const y = 2;

function bar() {
const z = 3;
console.log(z);
}

return bar;
}

const bar = foo();
bar();
```

위 예제의 중첩 함수 `bar`는 외부 함수 `foo`보다 더 오래 유지되지만, 상위 스코프의 어떤 식별자도 참조하지 않는다.
이런 경우 대부분의 모던 브라우저는 메모리 낭비를 막기 위해 최적화를 통해 상위 스코프를 기억하지 않으므로 `bar` 함수는 클로저라고 할 수 없다.

```javascript
function foo() {
const x = 1;

function bar() {
console.log(x);
}
bar();
}

foo();
```

위 예제의 중첩 함수 `bar`는 상위 스코프의 식별자를 참조하므로 클로저이지만, 외부 함수 `foo`의 외부로 중첩 함수 `bar`가 반환되지 않는다.
다시 말해 외부 함수 `foo`보다 중첩 함수 `bar`의 생명 주기가 짧으며 이런 경우 생명 주기가 종료된 외부 함수의 식별자를 참조할 수 있다는 클로저의 본질에 부합하지 않는다.
따라서 이런 경우에도 중첩 함수 `bar`는 일반적으로 클로저라고 하지 않는다.

```javascript
function foo() {
const x = 1;
const y = 2;

function bar() {
console.log(x);
}
return bar;
}

const bar = foo();
bar();
```

위 예제의 중첩 함수 `bar`는 상위 스코프의 식별자를 참조하고 있고, 외부 함수의 외부로 반환되어 외부 함수보다 생명 주기가 길기 때문에 클로저라고 부른다.
**클로저는 이렇게 중첩 함수가 상위 스코프의 식별자를 참조하고 있고 중첩 함수가 외부 함수보다 더 오래 유지되는 경우에 한정하는 것이 일반적이다.**
클로저에 의해 참조되는 상위 스코프의 변수(위의 경우에는 `x`)를 **자유 변수**라고 부른다.
모던 자바스크립트 엔진은 최적화가 잘 되어 있어서 클로저가 참조하고 있지 않는 식별자는 기억하지 않기 때문에 위 예제에서의 `y`는 기억하지 않는다.
클로저의 메모리 점유는 필요한 것을 기억하기 위한 것이므로 이는 불필요한 메모리 낭비라고 걱정할 대상이 아니다.
Loading