(JS) 함수 2. Call pattern, 생성자 대안(함수형 패턴), 상속
함수 객체
함수도 객체이기 때문에 다른 값들처럼 배열에 저장하거나 인수, 반환값으로 사용할 수 있다.
함수 객체는 Function.prototype
에 연결된다. ( Function
이 Object.prototype
에 연결되어 있다. )
모든 함수는 두 개의 숨겨진 속성 context
과 code
를 가지고 있다.
또한, 모든 함수 객체는 prototype
객체를 속성으로 가지고 있다. 이 객체는 다시 함수 자기 자신(this
) 자체를 값으로 갖는 constructor
라는 속성을 가지고 있다.
* 함수 객체가 만들어질 때, 함수를 생성하는 Function 생성자는 다음과 같은 코드를 실행한다.
1
this.prototype = {constructor: this};
Call pattern
모든 함수는 명시되어 있는 매개변수에 더해서 this
와 arguments
라는 추가적인 매개변수 두 개를 받게 된다. 함수 호출 패턴으로는 다음 네 가지가 존재하며 각각의 패턴에 따라 this
다르게 초기화되기 때문에 매우 주의해야 한다.
method call pattern
func.method()
단순히 메소드를 호출하는 경우라고 생각하면 안되고, 세부지정( .
이나 []
)을 이용하여 호출하는 경우를 말한다.
this
는 메소드를 포함하고 있는 객체에 바인딩된다. (this
는 객체 자체가 된다.)
this
와 객체의 바인딩은 호출 시에 일어난다.
function call pattern
func();
세부지정 없이 함수 이름만으로 호출하는 경우. this
는 전역 객체에 바인딩된다.
메소드나 어떤 함수 안에 위치한 내부 함수 일지라도 이렇게 호출하면 함수 호출 패턴이 적용된다.
이런 특성은 언어 설계 단계에서의 실수라고 한다. 원래는 내부 함수를 호출할 때 이 함수의 this
는 외부 함수의 this
변수에 바인딩되어야 맞는거라고.
이 때문에 new
없이 그냥 함수를 호출하는 경우 this
가 전역 객체에 바인딩되어 알 수 없는 결과가 초래된다.
이를 해결하는 방법은 메소드에서 변수(관례상 that
)를 정의한 후 여기에 this
를 할당하고, 이 변수를 통해서 this
에 접근하는 방법이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
var bObject = {
value: 0,
add_value: function ( ){
var that = this;
var helper = function ( ) {
that.value += 3;
}
helper();
}
};
bObject.add_value();
======
bObject.value : 3
(global) value : error ( not defined )
만약 여기서 that
을 안쓰고 this
로 접근한다면 helper();
를 호출할 때 전역 객체에 바인딩되어 bObject.value : 0
이 된다.
* typeof
가 function
이 아니라 object
여야만 제대로 동작한다. 아래와 같은 상황에서는 that
을 사용해도 제대로 동작하지 않는다.
1
2
3
4
5
6
7
8
9
10
11
12
aFunc = function ( ){
var value = 0;
var that = this;
function helper () {
that.value = 3;
};
helper();
return value;
}();
======
(aFunc's) value : 0
(global) value : 3
이는 bObject, aFunc
모두 내부의 helper()
에는 function call pattern이 적용되지만 bObject.add_value()
는 method call pattern이 적용되어 this
에 bObject
가 바인딩되고, aFunc
는 function call pattern이 적용되어 this
에 전역 객체가 바인딩되기 때문이다. 그래서 후자의 경우 that
에 전역 객체가 바인딩되기 때문에 제대로 동작하지 않는다.
apply call pattern
함수는 apply(bind\_to\_this, arg_array)
라는 메소드를 가지고 있으므로 이를 호출해 this
와 arguments
를 지정할 수 있다. 이를 이용하면 어떤 객체에 다른 객체의 메소드를 적용할 수 있다. 근데 아마 .prototype
에 연결된 메소드에 대해서만 사용할 수 있는 듯. * apply()
는 this
를 포함하고 있는 어떤 메소드를 빌려쓸 수 있도록 한다. 객체 생성 시점에 prototype을 지정하는 메소드는 apply()
가 아니라 Object.prototype()
이다.
constructor call pattern
JS에는 클래스가 없다. 프로토타입을 이용해 상속한다.
new
문법은 클래스 기반 언어에서 사용하는 생성자 호출 방식이다. 이는 JS의 프로토타입적 속성을 애매하게 만들기 때문에, new와 constructor function을 사용하는 스타일은 권장 사항이 아니다. 아래에 대안이 있다.
new
와 함께 사용하도록 만든 함수를 생성자(constructor)라고 하며 보통 파스칼 표기법으로 표기한다. new
를 사용하면 호출된 함수의 prototype
속성의 값에 연결되는 (숨겨진) 링크를 갖는 객체가 생성되고, 이 새로운 객체는 this
에 바인딩된다.
데이터는 생성자 함수 안에, 메소드는 prototype
으로 지정하는 방식이다.
1
2
3
4
5
6
7
8
9
var Cobject = function (v){
this.value = v;
};
// function도 Object 이기 때문에, 아래 처럼 Cobject라는 function이 prototype이라는 field를 가질 수 있다!
Cobject.prototype.get_value = function ( ) {
return this.value;
};
var c = new Cobject(9);
alert(c.get_value());
new
연산자를 생성자를 포함하는 객체의 메소드라고 가정하면 이런 식으로 표현된다.
1
2
3
4
5
6
7
8
9
Function.method('new', function ( ) {
//생성자 함수가 속해있는 객체의 prototype 속성(this.prototype)을
//프로토타입으로 하는 새로운 객체를 만들고 ( == 상속 )
var that = Object.create(this.prototype);
//이를 this에 바인딩한 다음
var other = this.apply(that, arguments);
//새로운 객체를 리턴
return (typeof other === 'object' && other) || that;
});
이 방식은 보편적으로 쉽게 이해할 수 있다는 장점이 있다. 그러나 new
를 빼먹어도 런타임 에러가 발생하지 않는데, new
를 빼먹는 경우 this
가 전역 객체와 바인딩되기 때문에 의도한 대로 동작하지 않아 실수할 수 있는 여지가 있다.
[!info] ECMAScript 6 에서
class
키워드가 추가되어서, 위 과정을 class 키워드를 통해서 익숙한 방법으로 처리할 수 있다.
다만, 그냥 키워드만 추가된 것이고 내부적으로는 위와 같이 처리하게 된다.
생성자 대안
JS에서 객체를 찍어내는 다양한 방법이 있다.
- 객체 리터럴
new
와Constructor
Object.create()
메소드를 이용한 프로토타입 방식- (추천, 함수형 패턴) 객체를 생성 및 반환하는 함수를 호출하는 방법
가장 추천하는 방식은 프로토타입 방식(Object.create()
)을 응용한 객체를 생성 및 반환하는 함수를 사용한 방식이다.
객체를 생성 및 반환하는 함수는 다음 4 단계로 진행된다.
#1 필요한 private
변수와 메소드를 정의한다.
#2 that
에 새로운 객체를 생성해 할당한다.
#3 that
에 메소드를 추가한다.
#4 that
을 반환한다.
new
를 사용하지 않기 때문에 대문자로 시작하는게 아니라 소문자로 시작하도록 정의한다.
1
2
3
4
5
6
7
8
var constructor = function (spec, my) {
var that, private 변수와 메소드;
my = my || {};
공유할 변수와 메소드를 my에 추가
that = 새로운 객체
that에 메소드 추가
return that;
}
일반적으로 constructor
를 호출할 때 넘기게 되는 데이터는 spec
을 통해 넘기면 된다. my
는 선택적으로 사용하면 되며 상속 연결상에서 생성자와 공유하게 되는 비밀을 담는 컨테이너로 사용한다.
이런 식으로 사용하면 된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var createMammal = function (spec) {
var that = {};
that.get_name = function ( ) {
return spec.name;
}
/*이런 식으로 일단 private 메소드로 만든 다음 that에 할당할 수도 있다.
이렇게 되면 내부에서는 that.says이 아니라 says로 호출할 수 있고,
that.says가 변경되더라도 says는 그대로이므로
says를 호출하는 메소드는 같은 작업을 수행할 수 있게 된다.*/
says = function ( ) {
return spec.saying || '';
}
that.says = says;
return that;
};
var myMammal = createMammal({name: 'Herb'});
* function call pattern이기 때문에 var that = this
로 써도 소용없다.
또는 이런 식으로…
1
2
3
4
5
6
7
8
9
10
export const createX = () => {
private 변수와 메소드;
return {
field: {},
method() {
...
}
}
}
상속
#2 that
에 새로운 객체를 생성해 할당한다. 부분에서, that
에 상속 대상 객체를 생성해 할당하는 방식으로 처리한다.
1
2
3
4
5
6
var cat = function (spec) {
spec.saying = spec.saying || 'meow';
var that = mamml(spec);
...
return that;
};
함수형 패턴을 사용하면 super 메소드에도 접근 가능하다. 다음은 super 메소드를 함수로 반환하는 superior
메소드다. superior
메소드가 반환하는 함수는 속성이 변경되더라도 원래의 super 메소드를 반환한다.
1
2
3
4
5
6
7
Object.method('superior', function (name) {
var that = this,
method = that[name];
return function ( ) {
return method.apply(that, arguments);
};
});
superior
는 method call pattern이 적용되어 그냥 this
를 사용해도 되지만, 상속받는 함수에서 super 객체를 that
으로 받기 때문에 이렇게 사용한 듯.
이런 식으로 사용한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var mammal = function (spec) {
spec.saying = spec.saying || '';
var that = {};
that.get_name = function ( ) {
return spec.name;
}
return that;
};
var cat = function (spec) {
spec.saying = spec.saying || 'meow';
var that = mammal(spec),
super_get_name = that.superior('get_name');
that.get_name = function (n) {
return 'SUPER ' + super_get_name( );
};
return that;
};
var myCat = cat({name: 'Mint'});
alert(myCat.get_name( )); // SUPER Mint
superior
를 사용하지 않고 직접 super\_get\_name() = that.get\_name
한다면, 이 것이 that.get_name
을 다시 정의하는 부분 보다 위에 있으면 잘 동작한다. 그러나 재정의하는 부분 보다 아래에 위치하는 경우에는 재귀 호출이 되어 무한 루프에 빠진다. superior
메소드를 사용하면 위치에 상관 없이 제대로 super 메소드를 가져온다.