자바스크립트 엔진은 함수를 어디서 호출했는지가 아니라 함수를 어디에 정의 했는지에 따라 상위 스코프를 결정한다. 이를 렉시컬 스코프(정적 스코프)라 한다.
렉시컬 환경의 "외부 렉시컬 환경에 대한 참조"에 저장할 참조값, 즉 상위 스코프에 대한 참조는 함수 정의가 평가되는 시점에 함수가 정의된 환경(위치)에 의해 결정된다. 이것이 바로 렉시컬 스코프다.
const x = 1;
function foo() {
const x = 10;
// 상위 스코프는 함수 정의 환경(위치)에 따라 결정된다.
// 함수 호출 위치와 상위 스코프는 아무런 관계가 없다.
bar();
}
// 함수 bar는 자신의 상위 스코프, 즉 전역 렉시컬 환경을 [[Environment]]에 저장하여 기억한다.
function bar() { // 쉽게 말해서 자신보다 상위스코프, 즉 전역 스코프를 상위 스코프로 사용한다.
console.log(x);
console.log(x);
}
foo(); // 1
bar(); // 1
[예제 24-05]
const x = 1;
// 1)
function outer() {
const x = 10;
const inner = function () { console.log(x); } // 2)
return inner;
}
// outer 함수를 호출하면 중첩 함수 inner를 반환한다.
// 그리고 outer 함수의 실행 컨텍스트는 실행 컨텍스트 스택에서 팝되어 제거된다.
const innerFunc = outher(); // 3)
innerFunc(); // 10
위 코드의 실행 결과는 outer 함수의 지역 변수 x의 값인 10이다. 이미 생명주기가 종료되어 실행 컨텍스트 스택에서 제거된 outer 함수의 지역 변수 x가 다시 불활이라도 한 듯이 동작하고 있다.
이처럼 외부 함수보다 중첩 함수가 더 오래 유지되는 경우 중첩 함수는 이미 생명주기가 종료한 외부 함수의 변수를 참조할 수 있다. 이러한 중첩 함수를 클로저(closure) 라고 부른다.
"즉, 클로저 환경을 사용하면 단계에 상관없이 상위스코프에 있는 변수 또는 함수를 참조할 수 있다."
클로저는 중첩 함수가 상위 스코프의 식별자를 참조하고 있고 중첩 함수가 외부 함수보다 더 오래 유지되는 경우에 한정하는 것이 일반적이다.
클로저에 의해 참조되는 상위 스코프의 변수를 자유 변수(free variable)라고 부른다.
클로저란 "함수가 자유 변수에 대해 닫혀있다"라는 의미이다.
[예제 24=09]
// 카운트 상태 변수
let num = 0;
// 카운트 상태 변경 함수
const increase = function () {
// 카운트 상태 변경 함수
return ++num;
}
console.log(increase()); // 1
console.log(increase()); // 2
위 코드는 잘 동작하지만 오류를 내포할 가능성이 있다. 이 코드가 바르게 동작하려면 다음의 전제 조건이 지켜져야 하기 때문이다.
클로저를 활용해 보자.
[예제 24-11]
// 카운트 상태 변경 함수
const increase = (function () {
// 카운트 상태 함수
let num = 0;
// 클로저
return function () {
// 카운트 상태를 1만큼 증가시킨다.
return ++num;
}
}());
console.log(increase()); // 1
console.log(increase()); // 2
이처럼 클로저는 상태(state)가 의도치 않게 변경되지 않도록 안전하게 은닉(information hiding)하고 특정 함수에게만 상태 변경을 허용하여 상태를 안전하게 변경하고 유지하기 위해 사용한다.
[예제 24-12]
// 카운트 상태 변경 함수
const increase = (function () {
// 카운트 상태 함수
let num = 0;
// 클로저인 메서드를 갖는 객체를 반환한다.
// 객체 리터럴은 스코프를 만들지 않는다.
// 따라서 아래 메서드들의 상위 스코프는 즉시 실행 함수의 렉시컬 환경이다.
return {
// num: 0, // 프로퍼티는 public하므로 은닉되지 않는다.
increase() {
return ++num;
},
decrease() {
return num > 0 ? --num : 0;
}
};
}());
console.log(decrease()); // 0
console.log(increase()); // 1
위 예제를 생성자 함수로 표현하면 다음과 같다.
[예제 24-13]
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.decrease()); // 0
console.log(counter.increase()); // 1
[예제 24-14]
// 함수를 인수로 전달받고 함수를 반환하는 고차함수
// 이 함수는 카운트 상태를 유지하기 위한 자유 변수 counter를 기억하는 클로저를 반환한다.
function makeCounter(aux) {
// 카운트 상태를 유지하기 위한 변수 counter를 기억하는 클로저를 반환한다.
let counter = 0;
// 클로저 반환
return function () {
// 인수로 전달받은 보조 함수에 상태 변경을 위임한다.
counter = aux(counter);
return counter;
}
}
// 보조 함수
function increase(n) {
retrun ++n;
}
// 보조 함수
function decrease(n) {
return --n;
}
// 함수로 함수를 생성한다.
// makeCounter 함수는 보조 함수를 인수로 전달받아 함수를 반환한다.
const increaser = makeCounter(increase); // 콜백 함수
console.log(increaser()); // 1
const decreaser = makeCounter(decrease); // 콜백 함수
console.log(decreaser()); // 0
makeCounter 함수를 호출해 함수를 반환할 떄 반환된 함수는 자신만의 독립된 렉시컬 환경을 갖는다.
캡슐화(encapsulation)는 객체의 상태(state)를 나타내는 프로퍼티와 프로퍼티를 참조하고 조작할 수 있는 동작(behavior)인 메서드를 하나로 묶는 것을 말한다.
캡슐화는 객체의 특정 프로퍼티나 메서드를 감출 목적으로 사용하기도 하는데 이를 정보 은닉(information hiding)이라 한다.
[예제 24-16]
function Person(name, age) {
this.name = name; // public
let _age = age; // private
// 인스턴스 메서드
this.sayHi = function () {
console.log(`Hi! I'm ${this.name}, ${_age} years old.`);
}
}
const me = new Person('Lee', 20);
me.sayHi(); // Hi! I'm Lee, 20 years old.
console.log(me.name); // Lee
console.log(me. _age); // undefined
위 예제의 sayHi 메서드는 인스턴스 메서드이므로 Person 객체가 생성될 때마다 중복 생성된다.(중복된 메모리 생성)
sayHI 메서드를 프로토타입 메서드로 변경하여 sayHi 메서드의 중복 생성을 방지해 보자.
[예제 24-17]
function Person(name, age) {
this.name = name; // public
let _age= age; // private
}
// 프로토타입 메서드
Person.prototype.sayHi = function () {
// Person 생성자 함수의 지역 변수 _age를 찹조할 수 없다.
console.log(`Hi! I'm ${this.name}, ${_age} years old.`);
}
이떄 Person.prototype.sayHi 메서드 내에서 Person 생성자 함수의 지역 변수 _age를 참조할 수 없는 문제가 발생한다.
따라서 다음과 같이 즉시 실행 함수를 사용하여 Person 생성자 함수와 Person.prototype.sayHi 메서드를 하나의 함수 내에 모아 보자.
[예제 24-18]
const Person = (function () {
let _age = 0; // pribate
// 생성자 함수
function Person(name, age) {
this.name = name; // public
_age = age;
}
// 프로토타입 메서드
Person.prototype.sayHi = function () {
console.log(`Hi! I'm ${this.name}, ${_age} years old.`);
}
return Person;
}());
const me = new Person('Lee', 20);
me.sayHi(); // Hi! I'm Lee, 20 years old.
console.log(me.name) // Lee
console.log(me._age) // undefined
[예제 24-20]
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
}
for 문의 변수 선언문에서 var 키워드로 선언한 i 변수는 블록 레벨 스코프가 아닌 함수 레벨 스코프를 갖기 떄문에 전역 변수다. 전역 변수 i에는 0, 1, 2가 순차적으로 할당된다.
ES6의 let 키워드를 활용하면 간단하게 해결된다.
[예제 24-22]
let 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