vaguely

和歌山に戻りました。ふらふらと色々なものに手を出す毎日。

【TypeScript】Decoratorを覗き見る 2

はじめに

今回は Decorator Factory から見てみますよ。

Decorator Factory

正直ドキュメントを見ても Class Decorator を返す関数、というだけでどう活用できるものなのか今一つ分かっていなかったのですが、これを使うことで、 Decorator に引数を渡すことができるようになります。

decoratorFactorySample.ts

import { Sample } from "../sample";

export function decoratorFactorySample(name: string, id: number, s: Sample){
    return function decoratorSample(constructor: Function){
        console.log("generate id:" + id + " name:" + name + " " + constructor);
    }
}

operationSample.ts

import { decoratorFactorySample } from "./decoratorSamples/decoratorFactorySample";

@decoratorFactorySample("john", 1, {call: () => console.log("hello sample"), message: "goodomorning"})
export class OperationSample{
    public constructor(){
        console.log("constructor death");
    }
    public sayHello(){
        console.log("hello world!");
    }
}

引数の個数や種類に制限は無いようで、上記のように増やしても問題なく実行できました。

逆に引数を 0 にすると。。。

operationSample.ts

import { decoratorFactorySample } from "./decoratorSamples/decoratorFactorySample";

@decoratorFactorySample()
export class OperationSample{
    public constructor(){
        console.log("constructor death");
    }
    public sayHello(){
        console.log("hello world!");
    }
}

おや?これ InversifyJS を利用するコードで見たことあるような。。。
(まだコードを読んでいないので確証はありませんが)

なお当然とも言えますが、この後登場する Method Decorator など Class 以外の Decorator でも使用できます。

Method Decorator

お次は Method Decorator です。

名前の通りメソッドに付与する Decorator ですが、今度は引数が 3 つあります。

methodDecoratorSample.ts

export function methodDecoratorSample(target: any, propertyKey: string, discripter: PropertyDescriptor){
    console.log("method decorator start");
    console.log(target);
    console.log(propertyKey);
    console.log(discripter);    
    console.log("method decorator end");
}

operationSample.ts

import { methodDecoratorSample } from "./decoratorSamples/methodDecoratorSample";

export class OperationSample{
    public constructor(){
        console.log("constructor death");
    }
    @methodDecoratorSample
    public sayHello(){
        console.log("hello world!");
    }
}

実行すると下記のようになります。

method decorator start
Object { sayHello: sayHello(), … }
sayHello
Object { value: sayHello(), writable: true, enumerable: true, configurable: true }
method decorator end

第一引数はメソッドを定義しているクラス、第二引数は対象のメソッド名、第三引数はメソッドの情報、ということのようです。

出力結果を見ると、 PropertyDescriptor.value は対象のメソッドであるため、 PropertyDescriptor.value.call(target) のようにすれば呼び出すことができます。

が、ドキュメントによると target を ES5 より低く設定している場合は PropertyDescriptor が undefined になるようなので、 ES3 に設定している、またはその恐れがある場合は使わない方が良いかもしれません。

なお Mastering TypeScript 3 では第三引数を optional にしていました(現行バージョンとの違いによるものかもしれませんが)。

なお、 PropertyDescriptor.value を使う以外に関数を実行する方法として、下記があります。

target[propertyKey]();

関数の呼び方

ここまで同じ関数(メソッド)を呼ぶのにいくつかの方法が登場しました。

例えばこんなクラスがあったとして

export class Sample{
    private message = "たこやき";
    public callMethod(){
        console.log("I love " + this.message);
        this.message = "チーズ";
    }
    public static callStaticMethod(){
        console.log("I love " + Sample.prototype.message);
        Sample.prototype.message = "コーンポタージュ";
    }
}

これらのメソッドを呼ぶ方法として下記が挙げられます
( 6 、 7 、 8 は 2 、 3 、 4 と同じですが、static メソッドということで追加しました)。

export function doSomething(){
    let sample = new Sample();
    // 1
    sample.callMethod();
    // 2
    sample.callMethod.call(sample);
    // 3
    sample.callMethod.apply(sample);
    // 4
    sample["callMethod"]();
    // -- static method --
    // 5
    Sample.callStaticMethod();
    // 6
    Sample.callStaticMethod.call(Sample);
    // 7
    Sample.callStaticMethod.apply(Sample);
    // 8
    Sample["callStaticMethod"]();
}

1 、5 は C# でもおなじみの方法ですが、他はあまり見慣れない呼び方です。

実行結果は下記の通りです(どれがどの結果かわかるように番号を振っています)。

// 1
I love たこやき
// 2
I love チーズ
// 3
I love チーズ
// 4
I love チーズ
// 5
I love undefined
// 6
I love コーンポタージュ
// 7
I love コーンポタージュ
// 8
I love コーンポタージュ

1 ~ 4 までのインスタンスを作って呼んでいるメソッド( prototype メソッド)は、呼び方はともかく想定通りの動きではあります。

5 は2回繰り返すと、他の static メソッド同様「I love コーンポタージュ」となります。

ということは、プロパティ message の定義時に渡している「たこやき」を受け取ることはできず、 かつ static メソッド間では状態が保持されていることがわかります。

なお最後に 1 を追加するともう一度「I love チーズ」と出力されるため、 prototype メソッド側(というか生成したインスタンス)も static メソッドの状態を見ていないことがわかります。

早速それぞれのメソッドを見ていきたいのですが( prototype も気になる)、その前に(見た目上)なじみのある書き方である 1 、5 について。

実は、 JavaScript(TypeScript) におけるクラス、というのは、関数の糖衣構文であるとのこと。

そのため先ほどのクラスは、 ES3 で変換すると下記のようになります( TypeScript Playground で確認) 。

"use strict"
var Sample = /** @class */ (function () {
    function Sample() {
        this.message = "たこやき";
    }
    Sample.prototype.callMethod = function () {
        console.log("I love " + this.message);
        this.message = "チーズ";
    };
    Sample.callStaticMethod = function () {
        console.log("I love " + Sample.prototype.message);
        Sample.prototype.message = "コーンポタージュ";
    };
    return Sample;
}());

インスタンスを作って呼んでいる callMethod はその名の通り prototype を通して、 static メソッドである callStaticMethod はそのまま Sample につながっています。

いつも通り迷走感がすごいですが、今回は prototype を見てみることにしましょう。

Prototype

上記を見ると、まず JavaScript では、関数(メソッド)・プロパティなどの種類にかかわらず、そのほとんどが prototype という Object を継承していると。

先ほどの実験から prototype メソッドと static メソッドで見ているものが違う、ということはわかりました。

ではそれぞれどう違うのか、とりあえずコンソールに出力して比べてみます。

まずは prototype メソッドから。

mainPage.ts

export function doSomething(){

    let sample = new Sample();

    console.log(sample.callMethod);
    console.log(sample.callMethod.prototype);
}

結果はこちら。

// console.log(sample.callMethod);
callMethod()​
    length: 0
    name: ""
    prototype: {…}
        constructor: function callMethod()​​
        < prototype>: Object { … }
    < prototype>: ()
        apply: function apply()
        arguments: 
        bind: function bind()
        call: function call()
        caller: 
        constructor: function Function()
        length: 0
        name: ""
        toSource: function toSource()
        toString: function toString()
        Symbol(Symbol.hasInstance): function Symbol.hasInstance()
        < get arguments()>: function arguments()
        < set arguments()>: function arguments()
        < get caller()>: function caller()
        < set caller()>: function caller()
        < prototype>: Object { … }

// console.log(sample.callMethod.prototype);
{…}
    constructor: function callMethod()​
    < prototype>: {…}
        __defineGetter__: function __defineGetter__()
        __defineSetter__: function __defineSetter__()
        __lookupGetter__: function __lookupGetter__()
        __lookupSetter__: function __lookupSetter__()
        __proto__: 
        constructor: function Object()
        hasOwnProperty: function hasOwnProperty()
        isPrototypeOf: function isPrototypeOf()
        propertyIsEnumerable: function propertyIsEnumerable()
        toLocaleString: function toLocaleString()
        toSource: function toSource()
        toString: function toString()
        valueOf: function valueOf()
        < get __proto__()>: function __proto__()
        < set __proto__()>: function __proto__()

では static メソッド。

mainPage.ts

export function doSomething(){
    console.log(Sample.callStaticMethod);
    console.log(Sample.callStaticMethod.prototype);
}

結果です。

// console.log(Sample.callStaticMethod);
callStaticMethod()​
    length: 0
    name: ""
    prototype: {…}
        constructor: function callStaticMethod()​​
        < prototype>: Object { … }
    < prototype>: ()
        apply: function apply()
        arguments: 
        bind: function bind()
        call: function call()
        caller: 
        constructor: function Function()
        length: 0
        name: ""
        toSource: function toSource()
        toString: function toString()
        Symbol(Symbol.hasInstance): function Symbol.hasInstance()
        < get arguments()>: function arguments()
        < set arguments()>: function arguments()
        < get caller()>: function caller()
        < set caller()>: function caller()
        < prototype>: Object { … }

// console.log(Sample.callStaticMethod.prototype);
{…}​
    constructor: function callStaticMethod()​
    < prototype>: {…}
        __defineGetter__: function __defineGetter__()
        __defineSetter__: function __defineSetter__()
        __lookupGetter__: function __lookupGetter__()
        __lookupSetter__: function __lookupSetter__()
        __proto__: 
        constructor: function Object()
        hasOwnProperty: function hasOwnProperty()
        isPrototypeOf: function isPrototypeOf()
        propertyIsEnumerable: function propertyIsEnumerable()
        toLocaleString: function toLocaleString()
        toSource: function toSource()
        toString: function toString()
        valueOf: function valueOf()
        < get __proto__()>: function __proto__()
        < set __proto__()>: function __proto__()

うーん。。。 メソッド名の違いを除くとあまり違っているようには見えませんね。。。

見てるところが違っているのかしら。。。

一旦切ります。