0%

201214_TIL(실행 컨텍스트)

오늘 배운 것

실행 컨텍스트

실행 컨텍스트 스택에서 전역 실행 컨텍스트가 pop되어도 window 객체는 GC에 삭제되지 않는다.

클로저

클로저는 상태(state)가 의도치 않게 변경되지 않도록 안전하게 은닉(information hiding)하고 특정 함수에게만 상태 변경을 허용하여 상태를 안전하게 변경하고 유지하기 위해 사용한다.

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

클로저는 외부함수와 중첩함수가 필요하고 외부함수가 먼저 종료되어 중첩함수가 외부함수 보가 더 오래 살아남아야한다.

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

increase, decrease를 품은 객체 리터럴은 상위 스코프로 즉시실행함수를 가지며 즉시실행 함수보다 오래 살아남기 때문에 클로저이다.

캡슐화와 정보 은닉

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

_age 변수는 private하다.

위 예제의 sayHi 메서드는 인스턴스 메서드이므로 Person 객체가 생성될 때마다 중복 생성된다. sayHi 메서드를 프로토타입 메서드로 변경하여 sayHi 메서드의 중복 생성을 방지해 보자.

1
2
3
4
5
6
7
8
9
10
function Person(name, age) {
this.name = name; // public
let _age = age; // private
}

// 프로토타입 메서드
Person.prototype.sayHi = function () {
// Person 생성자 함수의 지역 변수 _age를 참조할 수 없다
console.log(`Hi! My name is ${this.name}. I am ${_age}.`);
};

Person 생성자 함수의 지역 변수 _age를 참조할 수 없는 문제가 발생한다.

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
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

하지만 위 코드도 문제가 있다. Person 생성자 함수가 여러 개의 인스턴스를 생성할 경우 동일한 렉시컬 환경을 공유하기 때문에 다음과 같이 _age 변수의 상태가 유지되지 않는다는 것이다.

1
2
3
4
5
6
7
8
const me = new Person("Lee", 20);
me.sayHi(); // Hi! My name is Lee. I am 20.

const you = new Person("Kim", 30);
you.sayHi(); // Hi! My name is Kim. I am 30.

// _age 변수 값이 변경된다!
me.sayHi(); // Hi! My name is Kim. I am 30.

자주 발생하는 실수

1
2
3
4
5
6
7
8
9
10
11
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
}

funcs 배열의 요소로 추가한 함수를 호출하면 전역 변수 i를 참조하여 i의 값 3이 출력된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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]());
}

즉시 실행 함수의 매개변수 id는 즉시 실행 함수가 반환한 중첩 함수의 상위 스코프에 존재한다. 즉시 실행 함수가 반환한 중첩 함수는 자신의 상위 스코프(즉시 실행 함수의 렉시컬 환경)를 기억하는 클로저이고, 매개변수 id는 즉시 실행 함수가 반환한 중첩 함수에 묶여있는 자유 변수가 되어 그 값이 유지된다.

ES6의 let 키워드를 사용하면 이와 같은 번거로움이 깔끔하게 해결된다.

1
2
3
4
5
6
7
8
9
10
11
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
}

for 문의 변수 선언문에서 let 키워드로 선언한 변수를 사용하면 for 문의 코드 블록이 반복 실행될 때마다 for 문 코드 블록의 새로운 렉시컬 환경이 생성된다. 만약 for 문의 코드 블록 내에서 정의한 함수가 있다면 이 함수의 상위 스코프는 for 문의 코드 블록이 반복 실행될 때마다 생성된 for 문 코드 블록의 새로운 렉시컬 환경이다.

Nyong’s GitHub