프로그래밍/JS

7장) 함수 표현식

소복 2016. 3. 30. 17:16

이 장에서 다루는 내용

 - 함수 표현식의 특징

 - 함수와 재귀

 - 클로저를 이용한 고유 변수


5장에서 배웠듯이 함수를 정의하는 방법은 함수 선언과 함수 표현식 두 가지이다.


함수 선언의 뚜렷한 특징은 '함수 선언 끌어올림'이다.


함수 표현식에는 여러 가지 형태가 있는데 가장 많이 쓰이는 형태는 다음과 같다.

var funcName = function (arg) {    };

이렇게 생성된 함수는 함수 이름이 없으므로 익명 함수로 간주한다. (람다 함수라고 부르기도 함.)

따라서 익명함수의 name 프로퍼티는 빈 문자열이다.


if문 안에서도 함수 선언으로 정의된 함수는 끌어올려지므로 의도와 다르게 작동할 수 있어 주의해야 한다.


7.1 재귀

재귀 함수를 사용할 때는 항상 함수 이름 대신 arguments.callee()를 쓰는 것을 권한다.

ex)

function factor(num){

if(num <= 1){

return 1;

}else{

return num * factor(num-1);                // error!

return num * arguments.callee(num-1);    // success!

}

}

var another = factor;

factor = null;

alert(another(7));


7.2 클로저

'익명 함수'와 '클로저'는 자주 잘못 혼용된다.

'클로저'란 다른 함수의 스코프에 있는 변수에 접근 가능한 함수이다.

다음과 같이 만들 수 있다.

ex)

function createComparisonFunction(propertyName){

return function(obj1, obj2){

var num = [obj1.propertyName, obj2.propertyName];


if(num[0] < num[1]){

return -1;

}else if(num[0] > num[1]){

return 1;

}else{

return 0;

}

};

}


위 예제에서 강조한 부분은 반환된 함수가 내부, 익명 함수이면서 외부 함수의 변수에 접근한다는 것이다.

내부함수가 반환된어 다른 컨텍스트에서 실행되는 동안에도 propertyName에 접근할 수 있다.

왜냐하면 내부 함수의 스코프 체인에 createComparisonFunction의 스코프가 포함되기 때문이다.


함수를 호출하면 실행 컨텍스트와 스코프 체인이 생성된다. (실행 컨텍스트와 스코프 체인에 대해서 이미 4장에서 다룬 적이 있다. 링크)

함수의 활성화 객체는 arguments 및 이름 붙은 매개변수로 초기화된다.

외부 함수의 활성화 객체는 스코프 체인의 두 번째 객체이다.

이 과정이 포함 관계에 있는 함수에서 계속 발생하여 스코프 체인이 전역 실행 컨텍스트에서 종료될 때까지 이어진다.

함수를 실행하면 값을 읽거나 쓸 변수를 스코프 체인에서 검색한다.


스코프 체인 생성 과정)

함수를 호출하면 실행 컨텍스트와 스코프 체인이 생성된다.

함수의 활성화 객체는 스코프 체인 맨 앞에 arguments 및 이름 붙은 매개변수로 초기화된다.

외부 함수의 활성화 객체는 스코프 체인의 두 번째 객체이다.
이러한 과정이 포함 관계에 있는 함수에서 계속 발생하여 스코프 체인이 전역 실행 컨텍스트에서 종료될 때까지 이어진다.

전역 컨텍스트의 변수 객체는 항상 존재하지만, 로컬 컨텍스트 변수 객체는 함수를 실행하는 동안에만 존재한다.
스코프 체인은 변수 객체를 가리키는 포인터 목록이며 객체를 직접 포함하는 건 아니다.

다른 함수의 내부에 존재하는 함수는 외부 함수의 활성화 객체를 자신의 스코프 체인에 추가한다.
익명 함수의 스코프 체인의 맨 앞은 arguments 및 이름 붙은 매개변수이며
외부 함수가 실행을 마치고 익명 함수를 반환하면 익명 함수의 두 번째 부터는 외부 함수의 스코프 체인과 같은 값을 가진다.
(외부 함수의 활성화 객체와 전역 변수 객체)
이 때문에 익명 함수는 외부 함수의 변수 전체에 접근할 수 있는 것 이다.
한 가지 더 흥미로운 것은 외부 함수가 실행을 마쳤는데도 활성화 객체가 파괴되지 않는 점인데 아직 익명 함수의 스코프 체인에서 이름 참조하기 때문이다.
결론: 외부 함수가 실행을 마치면 실행 컨텍스트의 스코프 체인은 파괴되지만 활성화 객체는 익명 함수가 파괴될 때까지 메모리에 남는다.
이것의 메모리를 회수하려면 익명 함수가 저장된 변수에 null을 대입하면 된다.

7.2.1 클로저와 변수
스코프 체인의 한 가지 부작용이 있다.
클로저는 항상 외부 함수의 변수에 마지막으로 저장된 값만 알 수 있다.
클로저가 특정 변수가 아니라 전체 변수 객체에 대한 포인터를 저장함을 기억해라.
ex)
function createFunctions(){
var result = [];

for(var i=0; i < 10; i++){
result[i] = function(){
return i;            // i의 주소를 반환하므로 각각의 함수들은 모두 10을 반환한다.
};
}
}

< 올바르게 동작 시키는 방법 >
function createFunctions(){
var result = [];

for(var i=0; i < 10; i++){
result[i] = function(num){    // 함수의 매개변수는 값 형태로 전달되므로 num에 i값이 복사된다.
return function(){
return num;            // 고유한 num 값을 반환한다.
};
}(i);                        // 함수에 i를 num에 넣고 num이 반환되는 함수를 대입한다.
}
}

7.2.2 this 객체
클로저 내부의 this 객체는 복잡하게 작동한다.
this 객체는 런타임에서 함수가 실행 중인 컨텍스트에 묶인다.
즉 전역 함수에서 this는 스트릭트 모드가 아닐 때는 window, 스트릭트 모드에서는 undefined이며 함수가 객체 메서드로 호출되었을 때는 해당 객체이다.
이 컨텍스트에서 익명 함수는 특정 객체에 묶여 있지 않으므로 스트릭트 모드가 아니라면 this 객체는 window이며 스트릭트 모드에서는 undefined이다.
하지만 클로저를 작성하는 방식 때문에 이를 분명히 알기는 어렵다.
ex)
var name = "The Window";

var obj = {
name: "Mine",
getName: function(){
return function(){        // 익명 함수는 외부 스코프의 this 객체를 포함하지 않는다.
return this.name;
};
}
};

alert(obj.getName()());        // 전역 변수의 값을 반환한다.

모든 함수는 호출되는 순간 자동으로 this와 arguments를 갖게됨을 상기해라.
내부 함수는 결코 외부 함수의 this와 arguments에 직접적으로 접근할 수 없다.
다음과 같이 외부 함수의 this 객체를 다른 변수에 저장해서 클로저가 이 변수에 접근하도록 하는 일은 가능하다.
ex)
var name = "The Window";

var obj = {
name: "Mine",
getName: function(){
var that = this;
return function(){
return that.name;    // that은 여전히 obj에 묶여 있음
};
}
};

(obj.getName)();                    // "Mine"
(obj.getName = obj.getName)();    // "The Window" (스트릭트 모드가 아닐 때)

7.2.3 메모리 누수
(생략)

7.3 블록 스코프 흉내내기
자바스크립트에는 블록 레벨 스코프라는 개념이 없다.
따라서 블록 문장에서 정의한 변수는 외부함수에 묶이게 된다.
ex)
function output(count){
for(var i=0; i<count; i++){
alert(i);
}
alert(i);        // i는 output의 활성화 객체의 일부로 정의되므로 함수내에서 접근할 수 있다.
}

자바스크립트는 같은 변수를 선언하면 에러를 내지 않고 선언을 무시한다.
익명 함수를 통해 블록 스코프를 흉내 내서 이 문제를 해결할 수 있다.
이렇게 익명 함수를 블록 스코프처럼 쓰는 문법은 '고유 스코프'라고 부르기도 하며 다음과 같은 형태이다.
ex)
(function(){
// code block
})();
이 문법은 익명 함수를 정의하는 즉시 호출한다. '즉시 호출 함수'라고 부르기도 한다.
함수를 소괄호로 묶어주지 않으면 문법 에러를 일으킨다.
임시 변수가 필요할 때 마다 쓰면 된다.

이 테크닉은 전역 스코프에 추가되는 변수나 함수의 수를 제한하는 용도로 자주 사용한다.

7.4 고유 변수
자바스크립트에는 private member이란 개념이 없으며 객체 프로퍼티는 모두 public이다.
하지만 '고유 변수'라는 개념은 있다.
함수 안에서 정의한 변수는 함수 밖에서 접근할 수 없으므로 모두 고유 변수라고 간주한다.
함수 매개변수, 지역 변수, 내부 함수 등이 속한다.

함수 매개변수와 함수 내부 변수는 함수 밖에서 접근할 수 없다.
하지만 클로저를 활용해서 이러한 고유 변수에 접근 가능한 공용 메서드를 만들 수 있다.
이렇게 고유 변수/함수에 접근 가능한 공용 메서드를 '특권(privileged) 메서드'라고 한다.
객체에 특권 메서드를 만드는 방법은 두 가지이다.

첫 번째 방법은 생성자 안에서 만드는 방법이다.
ex)
function MyObj(){
privateVar = 10;
function privateFunction(){
return false;
}
// 특권 메서드
this.publicMethod = function(){
privateVar++;
return privateFunction();
};
}
이 패턴은 생성자 안에서 모든 고유 변수와 함수를 정의한다.
일단 MyObj의 인스턴스를 생성하면 privateVar과 privateFunction은 publicMethod에서만 접근이 가능하다.

다음과 같이 고유 및 특권 멤버를 정의해서 데이터를 직접적으로 수정할 수 없게 보호할 수 있다.
ex)
function Person(name){
this.getName = function(){
retrun name;
};
this.setName = function(val){
name = val;
};
}
var per = Person("It is Name");
이런 방법은 생성자를 호출할 때마다 메서드가 재생성되므로 고유 변수 name은 Person의 인스턴스마다 고유하다.
하지만 오직 생성자 패턴을 통해서만 이런 결과가 가능하다.
6장에 나오듯 생성자 패턴에는 인스턴스마다 메서드가 생성된다는 결점이 있다.
이러한 문제는 정적 고유 변수를 사용해 특권 메서드를 만들면 해결할 수 있다.

7.4.1 정적 고유 변수
특권 메서드를 고유 변수나 함수를 정의할 때 쓰는 고유 스코프를 통해서 생성하는 방법이다.
ex)
(function(){
// 고유 변수와 함수
var privateVar = 10;

function privateFunc(){
return flase;
}
// 생성자
MyObj = function(){
};
// 공용 메서드와 특권 메서드
MyObj.prototype.publicMethod = function(){
privateVar++;
return privateFunc();
};
})();
이 패턴에서는 생성자와 메서드를 감싸는 고유 스코프를 만들었다.
일반적인 프로토타입 패턴과 마찬가지로 공용 메서드는 프로토타입에 정의했다.
이 패턴에서는 생성자를 함수 표현식으로 정의했다.
함수 선언은 항상 지역 함수가 만들어지므로 이 패턴에서는 바람직하지 않다.
같은 이유로 MyObj에 var을 사용하지 않았다.
변수를 선언하지 않고 초기화하면 항상 전역 변수가 되므로 MyObj는 고유 스코프가 아니라 전역에 위치한다.
(스트릭트 모드에서는 선언하지 않고 할당하면 에러가 발생한다.)

이 패턴과 이전 패턴의 주요 차이는 고유 변수와 함수를 인스턴스에서 공유한다는 점이다.
특권 메서드는 프로토타입에 정의되므로 모든 인스턴스에서 같은 함수를 호출한다.
클로저가 될 특권 메서드는 외부 스코프에 대한 참조를 간직한다.
ex)
(function(){
var name="";

Person = function(val){
name = val;
};

Person.prototype.getName(){
return name;
};

Person.prototype.setName(val){
name = val;
};
})();
var per1 = new Person("A");
var per2 = new Person("B");
이 패턴을 쓰면 name 변수는 정적이 되고 모든 인스턴스에서 공유된다.
따라서 각 인스턴스가 독립 변수를 가질 수는 없지만 프로토타입을 통해 코드 재사용성은 좋아진다.
인스턴스를 쓸 것인지 정적 고유 변수를 쓸 것인지 상황에 따라 결정해야 한다.

7.4.2 모듈 패턴
더글러스 크록포드가 고안한 패턴이며 싱글톤에서 같은 일을 한다.
싱글톤이란 인스턴스를 단 하나만 갖게 의도한 객체이다.
전통적으로 싱글톤을 만들 때는 객체 리터럴 표기법을 사용했다.
모듈 패턴은 기본 싱글톤을 확장해서 고유 변수와 특권 메서드를 쓸 수 있다.
ex)
var singleton = function(){
// 고유 변수와 함수
var privateVar = 10;
function privateFunc(){
return false;
}

// 특권/공용 메서드와 프로퍼티
return {
publicProperty : true,
publicMethod : function(){
privateVar++;
return privateFunc();
}
};
}();
모듈 패턴은 객체를 반환하는 익명 함수를 사용한다.
객체 리터럴을 함수 값으로 반환한다. 반환된 객체 리터럴에는 공용이 될 프로퍼티와 메서드만 들어 있다.
객체는 익명 함수 내에서 정의되었으므로 공용 메서드는 모두 고유 변수와 함수에 접근 가능하다.
요약하자면 객체 리터럴이 싱글톤에 대한 공용 인터페이스를 정의하는 것이다.
이 패턴은 싱글톤에 일종의 초기화가 필요하고 고유 변수에 접근해야 할 때 유용하다.
ex)
var application = function(){
// 고유 변수와 함수
var components = new Array();

// 초기화
components.push(new BaseComponent());

// 공용 인터페이스
return {
getComponentCnt : function(){
return components.length;
},
registerComponent : function(component){
if(typeof component == "object"){
components.push(component);
}
}
};
}();
웹 앱에서는 앱 레벨의 정보를 관리할 싱글톤을 두는 경우가 많다.

모듈 패턴은 단 하나의 객체를 반드시 생성하고 몇 가지 데이터를 가지며 또한 고유 데이터에 접근 가능한
공용 메서드를 외부에 노출하도록 초기화해야 할 때 유용하다.
이런 식으로 생성한 싱글톤은 객체 리터럴을 이용했으므로 모두 Object의 인스턴스이다.

7.4.3 모듈 확장 패턴
모듈 패턴에서 한 가지 더 살펴볼 것은 객체를 반환하기 전에 확장하는 패턴이다.
이 패턴은 싱글톤 객체가 특정 타입의 인스턴스지만 프로퍼티나 메서드를 추가하여 확장해야 할 때 유용하다.
ex)
var singleton = function(){
//고유 변수와 함수
var privateVar = 10;

function privateFunc(){
return false;
}
//객체 생성
var obj = new Custom();
// 특권/공용 프로퍼티와 메서드 추가
obj.publicProperty = true;
obj.publicMethod = function(){
privateVar++;
privateFunc();
};

return obj;
}();

모듈 패턴 예제의 application 객체가 BaseComponent의 인스턴스여야 한다면 필요한 코드도 있지만 생략한다.

[출처: 프론트엔드 개발자를 위한 자바스크립트 프로그래밍]