서론
24.1 렉시컬 스코프
24.2 함수 객체의 내부 슬롯 [[Environment]]
- 함수는 내부 슬롯
[[Environment]]
에 자신이 정의된 환경을 저장- 자신이 정의된 환경 ⇒ 현재 실행 중인 실행 컨텍스트의 렉시컬 환경, 즉 상위 스코프
- 함수가 호출되었을 때 생성되는 함수 렉시컬 환경의 “외부 렉시컬 환경에 대한 참조”에 “내부 슬롯
[[Environment]]
에 저장한 렉시컬 환경의 참조”를 저장한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| var x = 1;
// 1. foo 함수 객체의 내부 슬롯 [[Environment]]에
// 전역 렉시컬 환경의 참조가 저장된다.
// 2. 외부 렉시컬 환경에 대한 참조에
// 내부 슬롯 [[Environment]]에 저장된 환경의 참조가 할당된다.
function foo() {
var x = 10;
bar();
}
// 1. bar 함수 객체의 내부 슬롯 [[Environment]]에
// 전역 렉시컬 환경의 참조가 저장된다.
// 2. 외부 렉시컬 환경에 대한 참조에
// 내부 슬롯 [[Environment]]에 저장된 환경의 참조가 할당된다.
function bar() {
console.log(x);
}
foo(); // 1
bar(); // 1
|
24.3 클로저와 렉시컬 환경
- 외부 함수보다 중첩 함수가 더 오래 유지되는 경우 중첩 함수는 생명 주기가 종료된 외부 함수의 변수를 참조할 수 있으며 이때 중첩 함수를 클로저라 한다.
클로저 예시
1
2
3
4
5
6
7
8
9
10
11
12
| const x = 1;
function outer() {
const x = 10;
const inner = function () { console.log(x); };
return inner;
}
// outer 함수를 호출하면 중첩 함수 inner를 반환한다.
// 그리고 outer 함수의 실행 컨텍스트는 실행 컨텍스트 스택에서 팝되어 제거된다.
const innerFunc = outer();
innerFunc(); // 10
|
inner
함수의 내부 슬롯 [[Environment]]
는 outer
함수의 렉시컬 환경을 참조하고 있다.inner
함수는 전역 변수 innerFunc
에 의해 참조되고 있으므로 가비지 컬렉션의 대상이 아니다.- 따라서
outer
함수의 렉시컬 환경은 유지된다.
모든 함수는 클로저일까?
- 이론적으로 자바스크립트의 모든 함수는 상위 스코프를 기억하므로 클로저이지만, 실질적으로 그렇지 않은 경우가 존재한다.
- 일반적으로 클로저는 상위 스코프의 식별자를 참조하고 있고, 외부 함수보다 더 오래 유지되는 중첩 함수를 말한다.
상위 스코프의 식별자를 참조하지 않는 경우
- 상위 스코프의 어떤 식별자도 참조하지 않는 중첩 함수는 클로저가 아니다.
- 상위 스코프의 어떤 식별자도 참조하지 않는 경우 모던 브라우저는 메모리 낭비를 막기 위해 최적화를 통해 상위 스코프를 기억하지 않는다.
- 자유 변수(free variable): 클로저에 의해 참조되는 상위 스코프의 변수
- 상위 스코프의 식별자를 참조하기 때문에 bar 함수는 클로저이며 x 변수는 자유 변수이다.
- 상위 스코프의 식별자를 참조하지 않기 때문에 bar 함수는 클로저가 아니다.
외부 함수보다 먼저 소멸하는 경우
- 일반적으로 외부 함수보다 일찍 소멸하는 중첩 함수는 클로저라 하지 않는다.
24.4 클로저의 활용
- 클로저는 상태를 안전하게 은닉하고(information hiding) 특정 함수에만 상태 변경을 허용
기존의 문제
1
2
3
4
5
6
7
8
9
10
11
12
| // 카운트 상태 변수
let num = 0;
// 카운트 상태 변경 함수
const increase = function () {
// 카운트 상태를 1만큼 증가시킨다.
return ++num;
};
console.log(increase()); // 1
console.log(increase()); // 2
console.log(increase()); // 3
|
아래 두 조건을 만족할 때만 위 코드는 올바르게 작동할 수 있다.
- 카운트 상태는
increase
함수가 호출되기 전까지 변경되지 않고 유지 - 카운트 상태는
increase
함수만이 변경 가능
하지만 카운트 상태는 전역 변수로 관리되어 의도치 않게 상태가 변경될 수 있다.
상태 변경을 방지하기 위한 해결 방법
1
2
3
4
5
6
7
8
9
10
11
12
13
| // 카운트 상태 변경 함수
const increase = function () {
// 카운트 상태 변수
let num = 0;
// 카운트 상태를 1만큼 증가시킨다.
return ++num;
};
// 이전 상태를 유지하지 못한다.
console.log(increase()); // 1
console.log(increase()); // 1
console.log(increase()); // 1
|
- 전역 변수 대신 지역 변수를 이용해 의도치 않은 상태 변경을 해결
- 하지만 이전 상태를 유지할 수 없는 문제 발생
상태 변경 방지 및 이전 상태 유지를 위한 해결 방법
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // 카운트 상태 변경 함수
const increase = (function () {
// 카운트 상태 변수
let num = 0;
// 클로저
return function () {
// 카운트 상태를 1만큼 증가시킨다.
return ++num;
};
}());
console.log(increase()); // 1
console.log(increase()); // 2
console.log(increase()); // 3
|
increase
변수에 클로저를 할당- 즉시 실행 함수를 이용해
num
변수가 다시 초기화되지 않도록 함 num
변수는 private
변수가 되어 의도되지 않게 변경되지 않음
클로저 응용
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| const counter = (function () {
// 카운트 상태 변수
let num = 0;
// 클로저인 메서드를 갖는 객체를 반환한다.
// 객체 리터럴은 스코프를 만들지 않는다.
// 따라서 아래 메서드들의 상위 스코프는 즉시 실행 함수의 렉시컬 환경이다.
return {
// num: 0, // 프로퍼티는 public하므로 은닉되지 않는다.
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
|
- 객체 리터럴의 중괄호는 코드 블록이 아니므로 스코프를 생성하지 않음
생성자 함수 응용
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| const Counter = (function () {
// 카운트 상태 변수
let num = 0;
function Counter() {
// this.num = 0; // 프로퍼티는 public하므로 은닉되지 않는다.
}
Counter.prototype.increase = function () {
return ++num;
};
Counter.prototype.decrease = function () {
return num > 0 ? --num : 0;
};
return Counter;
}());
const counter = new Counter();
console.log(counter.increase()); // 1
console.log(counter.increase()); // 2
console.log(counter.decrease()); // 1
console.log(counter.decrease()); // 0
|
increase
, decrease
메서드는 “실행 중인 실행 컨텍스트”인 “즉시 실행 함수 실행 컨텍스트”의 렉시컬 환경을 기억
함수형 프로그래밍
- 함수형 프로그래밍은 클로저를 적극 활용
- 외부 상태 변경, 가변 데이터를 피하고 불변성을 지향하기 때문
- 부수 효과를 억제하기 위함
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| // 함수를 인수로 전달받고 함수를 반환하는 고차 함수
// 카운트 상태를 유지하기 위한 자유 변수 counter를 기억하는 클로저를 반환한다.
function makeCounter(aux) {
// 카운트 상태를 유지하기 위한 자유 변수
let counter = 0;
// 클로저를 반환
return function () {
// 인수로 전달받은 보조 함수에 상태 변경을 위임한다.
counter = aux(counter);
return counter;
};
}
// 보조 함수
function increase(n) {
return ++n;
}
// 보조 함수
function decrease(n) {
return --n;
}
// 함수로 함수를 생성한다.
// makeCounter 함수는 보조 함수를 인수로 전달받아 함수를 반환한다
const increaser = makeCounter(increase);
console.log(increaser()); // 1
console.log(increaser()); // 2
// increaser 함수와는 별개의 독립된 렉시컬 환경을 갖기 때문에
// 카운터 상태가 연동하지 않는다.
const decreaser = makeCounter(decrease);
console.log(decreaser()); // -1
console.log(decreaser()); // -2
|
24.5 캡슐화와 정보 은닉
- 캡슐화: 객체의 상태를 나타내는 프로퍼티, 프로퍼티를 참조하고 조작할 수 있는 동작인 메서드를 하나로 묶는 것
- 객체의 특정 프로퍼티나 메서드의 정보 은닉을 위해 캡슐화 이용 가능
- 정보 은닉: 외부에 공개할 필요가 없는 구현의 일부를 감추는 것
- 적절치 못한 접근으로부터 객체의 상태 변경 방지 ⇒ 정보 보호
- 결합도(객체 간의 상호 의존성)를 낮춤
- 자바스크립트는 접근 제한자를 제공하지 않아 객체의 모든 프로퍼티와 메서드는 기본적으로
public
하다.
캡슐화와 정보 은닉 예시
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| function Person(name, age) {
this.name = name; // public
let _age = age; // private
// 인스턴스 메서드
this.sayHi = function () {
console.log(`Hi! My name is ${this.name}. I am ${_age}.`);
};
}
const me = new Person('Lee', 20);
me.sayHi(); // Hi! My name is Lee. I am 20.
console.log(me.name); // Lee
console.log(me._age); // undefined
const you = new Person('Kim', 30);
you.sayHi(); // Hi! My name is Kim. I am 30.
console.log(you.name); // Kim
console.log(you._age); // undefined
|
name
프로퍼티는 public
하다._age
변수는 Person
생성자 함수 외부에서 참조하거나 변경할 수 없으므로 private
하다.sayHi
메서드는 Person
객체가 생성될 때마다 중복으로 생성된다.
클로저를 이용한 캡슐화와 정보 은닉
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| const Person = (function () {
let _age = 0; // private
// 생성자 함수
function Person(name, age) {
this.name = name; // public
_age = age;
}
// 프로토타입 메서드
Person.prototype.sayHi = function () {
console.log(`Hi! My name is ${this.name}. I am ${_age}.`);
};
// 생성자 함수를 반환
return Person;
}());
const me = new Person('Lee', 20);
me.sayHi(); // Hi! My name is Lee. I am 20.
console.log(me.name); // Lee
console.log(me._age); // undefined
const you = new Person('Kim', 30);
you.sayHi(); // Hi! My name is Kim. I am 30.
console.log(you.name); // Kim
console.log(you._age); // undefined
// _age 변수 값이 변경된다.
me.sayHi(); // Hi! My name is Lee. I am 30.
|
Person
생성자 함수와 sayHi
메서드는 즉시 실행 함수의 지역 변수 _age
를 참조할 수 있는 클로저다.Person.prototype.sayHi
메서드의 상위 스코프는 어떤 인스턴스로 호출해도 동일한 상위 스코프를 사용하기 때문에 _age
변수의 상태는 유지되지 않는다.- 즉 자바스크립트는 정보 은닉을 완전하게 지원하지 못한다.
24.6 자주 하는 실수
1
2
3
4
5
6
7
8
9
| 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]()); // 3 3 3
}
|
for
문의 변수 선언문에서 i
변수는 함수 레벨 스코프를 갖기 때문에 전역 변수이다.
클로저를 이용한 해결
1
2
3
4
5
6
7
8
9
10
11
12
13
| 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]()); // 0 1 2
}
|
- 즉시 실행 함수의 매개 변수
id
는 즉시 실행 함수가 반환한 중첩 함수의 상위 스코프에 존재 - 즉시 실행 함수가 반환한 중첩 함수는 즉시 실행 함수의 렉시컬 환경을 기억하는 클로저
- 매개변수
id
는 즉시 실행 함수가 반환한 중첩 함수에 묶인 자유 변수 ⇒ 값 유지
let
키워드를 이용한 해결
let
키워드로 선언한 변수를 변수 선언문에 사용하면 코드 블록이 반복 실행될 때마다 for
문 코드 블록의 새로운 렉시컬 환경을 생성- 반복 실행이 될 때마다 식별자의 값(
for
문의 변수 선언문에서 선언된 초기화 변수, 코드 블록 내에서 선언한 지역변수 등)을 유지해야 하기 때문
for
문 코드 블록 내의 함수는 반복 실행될 때마다 새로운 렉시컬 환경을 상위 스코프로 가짐- 만약 반복문 코드 블록 내에 함수 정의가 없다면 반복문이 생성하는 새로운 렉시컬 환경은 반복 직후 가비지 컬렉션 대상이 된다.
1
2
3
4
5
6
7
8
9
| 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]()); // 0 1 2
}
|
let
키워드로 선언한 초기화 변수를 사용한 for
문 평가- 새로운 렉시컬 환경 생성, 초기화 변수 식별자와 값을 등록
- 새롭게 생성된 렉시컬 환경을 현재 실행 중인 실행 컨텍스트의 렉시컬 환경으로 교체
for
문의 코드 블록이 반복 실행될 때마다 아래 과정 반복- 새로운 렉시컬 환경 생성
for
문 코드 블록 내의 식별자와 값(증감문 반영 이전) 등록- 새롭게 생성된 렉시컬 환경을 현재 실행 중인 실행 컨텍스트의 렉시컬 환경으로 교체
- 반복 실행이 모두 종료되면
for
문 실행 이전의 렉시컬 환경을 현재 실행 중인 실행 컨텍스트의 렉시컬 환경으로 되돌림
고차 함수를 이용한 해결
- 변수와 반복문의 사용 억제 가능 ⇒ 오류가 줄고 가독성이 좋음
1
2
3
4
5
6
| // 요소가 3개인 배열을 생성하고 배열의 인덱스를 반환하는 함수를 요소로 추가한다.
// 배열의 요소로 추가된 함수들은 모두 클로저다.
const funcs = Array.from(new Array(3), (_, i) => () => i); // (3) [ƒ, ƒ, ƒ]
// 배열의 요소로 추가된 함수 들을 순차적으로 호출한다.
funcs.forEach(f => console.log(f())); // 0 1 2
|