Post

(JS) 함수 2. Call pattern, 생성자 대안(함수형 패턴), 상속

함수 객체

함수도 객체이기 때문에 다른 값들처럼 배열에 저장하거나 인수, 반환값으로 사용할 수 있다.
함수 객체는 Function.prototype에 연결된다. ( FunctionObject.prototype에 연결되어 있다. )
모든 함수는 두 개의 숨겨진 속성 contextcode를 가지고 있다.
또한, 모든 함수 객체는 prototype객체를 속성으로 가지고 있다. 이 객체는 다시 함수 자기 자신(this) 자체를 값으로 갖는 constructor라는 속성을 가지고 있다.

* 함수 객체가 만들어질 때, 함수를 생성하는 Function 생성자는 다음과 같은 코드를 실행한다.

1
this.prototype = {constructor: this};

Call pattern

모든 함수는 명시되어 있는 매개변수에 더해서 thisarguments라는 추가적인 매개변수 두 개를 받게 된다. 함수 호출 패턴으로는 다음 네 가지가 존재하며 각각의 패턴에 따라 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이 된다.

* typeoffunction이 아니라 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이 적용되어 thisbObject가 바인딩되고, aFunc는 function call pattern이 적용되어 this에 전역 객체가 바인딩되기 때문이다. 그래서 후자의 경우 that에 전역 객체가 바인딩되기 때문에 제대로 동작하지 않는다.

apply call pattern

함수는 apply(bind\_to\_this, arg_array)라는 메소드를 가지고 있으므로 이를 호출해 thisarguments를 지정할 수 있다. 이를 이용하면 어떤 객체에 다른 객체의 메소드를 적용할 수 있다. 근데 아마 .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에서 객체를 찍어내는 다양한 방법이 있다.

  • 객체 리터럴
  • newConstructor
  • 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 메소드를 가져온다.

This post is licensed under CC BY 4.0 by the author.