vaguely

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

【TypeScript】prototype を追いかける

はじめに

続きです。

が、 Decorator から離れていく気しかしないのでタイトルは変えておきます。

prototype メソッド

mainPage.ts

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 = "コーンポタージュ";
    }
}
~省略~

前回のコードですが、 prototype メソッド( callMethod() )が持つ Object(prototype) をたどってみるため、下記のようにしてみます。

class Sample{
    public callMethod(){
        ~省略~
    }
    
    public getThis(){
        return this;
    }
    
}

let sample = new Sample();

console.log(sample == sample.getThis());

console.log(sample.callMethod);
console.log(sample.callMethod.prototype);
console.log(Object.getPrototypeOf(sample.callMethod));
console.log(Object.getPrototypeOf(Object.getPrototypeOf(sample.callMethod)));
console.log(Object.getPrototypeOf(Object.getPrototypeOf(Object.getPrototypeOf(sample.callMethod))));

まず上記のコードにおいて、 prototype method である callMethod() や getThis() の中で this と書いた場合、その中身は Sample のインスタンスです( sample )。

そのため、sample.callMethod と callMethod() 内での this.callMethod も同じです。

ということで上記は、sample.callMethod のみで確認しています。

実行結果は以下の通り。

true
function callMethod()
undefined
function ()
Object { … }
null

console.log(sample.callMethod) の中身は function callMethod() となっています。

また、その下の Object.getPrototypeOf(sample.callMethod) を繰り返すところを見てみると、下記のようになっています。

function callMethod() > Function.prototype > Object > null

流れとしては関数(メソッド)のプロトタイプチェーンで説明されている通りかと思います。

{インスタンス}.{prototype メソッド}.prototype = undefined?

一つ気になるのが sample.callMethod.prototype 。

なんで undefined になるの?というか、こないだ実行したときそうなってなかったような。。。?と思っていたら、ターゲットを ES2015 以上に設定している場合、返還後の JavaScript でもクラスが使用されるのですが、その場合 undefined になるようです。

ES2015 未満

"use strict";
var Sample = /** @class */ (function () {
    function Sample() {
    }
    Sample.prototype.callMethod = function () {
    };
    Sample.prototype.getThis = function () {
        return this;
    };
    Sample.callStaticMethod = function () {
    };
    return Sample;
}());

ES2015 以上

"use strict";
class Sample {
    callMethod() {
    }
    getThis() {
        return this;
    }
    static callStaticMethod() {
    }
}

なお、 ES3 に設定した場合の sample.callMethod.prototype の結果は下記の通りです。

{…}
    constructor: function callMethod()​
    < prototype>: Object {…}

Object.getPrototypeOf(target) と target.prototype

若干今更感ありますが、 Object.getPrototypeOf(target) と target.prototype の違いとは何でしょうか。

Object.getPrototypeOf(target) (古くは proto ) は、 target の内部プロパティである Prototype を取得するものである、と。

内部プロパティというものが今一つ分かっていないのですが、実際の結果を見ると

console.log(sample.callMethod);
console.log(Object.getPrototypeOf(sample.callMethod));
console.log(Object.getPrototypeOf(Object.getPrototypeOf(sample.callMethod)));
console.log(Object.getPrototypeOf(Object.getPrototypeOf(Object.getPrototypeOf(sample.callMethod))));

この結果が

function callMethod()
function ()
Object { … }
null

メソッド > Punction Prototype > Object > null となっていることから、親の Prototype を取得できる、というイメージでしょうか。

では target.prototype は?というところですが、

この辺りをみると、自身( Constructor 関数)が持つ prototype プロパティであるようです。

…うーん、わかるようなわからないような。。。

この辺りはもう少し調べてみることにします。

prototype メソッドの prototype (図)

先ほどの Object.getPrototypeOf(target) を null が出るまで繰り返した結果を図にしておきます。

f:id:mslGt:20190910223456p:plain

static メソッド

次は static メソッドです。

console.log(Sample.callStaticMethod);
console.log(Object.getPrototypeOf(Sample.callStaticMethod));
console.log(Object.getPrototypeOf(Object.getPrototypeOf(Sample.callStaticMethod)));
console.log(Object.getPrototypeOf(Object.getPrototypeOf(Object.getPrototypeOf(Sample.callStaticMethod))));

結果は。。。

function callStaticMethod()
function ()
Object { … }
null

先ほどとほぼ同じですね。

Sample.callStaticMethod.prototype も同様に undefined になります。

また、 Object.getPrototypeOf(Sample.callStaticMethod)) で得られる Function prototype は prototype method のものと同じです。
(プロパティを追加すると、もう片方でも利用できます。すべきではないと思いますが)

static メソッドの prototype (図)

f:id:mslGt:20190910223525p:plain

関数

では関数だとどうでしょうか。

function callFunction(){   
}
console.log(callFunction);
console.log(Object.getPrototypeOf(callFunction));
console.log(Object.getPrototypeOf(Object.getPrototypeOf(callFunction)));
console.log(Object.getPrototypeOf(Object.getPrototypeOf(Object.getPrototypeOf(callFunction))));
console.log(callFunction.prototype);
console.log(Object.getPrototypeOf(callFunction.prototype));
console.log(Object.getPrototypeOf(Object.getPrototypeOf(callFunction.prototype)));

関数の場合は先ほどまでとは違い、 callFunction.prototype で値を得ることができます。

結果は下記の通り。

function callFunction()
function ()
Object { … }
null
{ … }
    constructor: function callFunction()​
    < prototype>: { … }
Object { … }
null

f:id:mslGt:20190910223610p:plain

Object.getPrototypeOf(target) で辿った結果だけを見ると、 prototype メソッドも static メソッドも関数も大きな違いはなさそうです。

関数だけ target.prototype が正しく取れるのはなぜ?と思いはしますが、メソッドの方は単にクラス構文が登場して、値の設定の仕方が変わったから、というだけのような気もします。

この辺も突っ込んで調べてみたいところ。

クラス

今度はクラスです。

JavaScript のクラスは関数の糖衣構文である。。。ということは。。。?

class Sample{
    public callMethod(){
    }
    public getThis(){
        return this;
    }
    public static callStaticMethod(){
    }
}
console.log(Sample);
console.log(Object.getPrototypeOf(Sample));
console.log(Object.getPrototypeOf(Object.getPrototypeOf(Sample)));
console.log(Object.getPrototypeOf(Object.getPrototypeOf(Object.getPrototypeOf(Sample))));
console.log(Sample.prototype);
console.log(Object.getPrototypeOf(Sample.prototype));
console.log(Object.getPrototypeOf(Object.getPrototypeOf(Sample.prototype)));
function Sample()
function ()
Object { … }
null
{ … }
    callMethod: function callMethod()​
    constructor: function Sample()​
    getThis: function getThis()​
    < prototype>: { … }
Object { … }
null

ほぼ関数と同じですね。

f:id:mslGt:20190910223708p:plain

クラスインスタンス

クラスインスタンスはどうでしょうか。

let sample = new Sample();

console.log(sample);
console.log(Object.getPrototypeOf(sample));
console.log(Object.getPrototypeOf(Object.getPrototypeOf(sample)));
console.log(Object.getPrototypeOf(Object.getPrototypeOf(Object.getPrototypeOf(sample))));

クラスインスタンスは target.prototype を持っていないようです。

結果はこちら。

{}
    < prototype>: Object { … }
{…}​
    callMethod: function callMethod()​
    constructor: function Sample()​
    getThis: function getThis()​
    < prototype>: Object { … }
Object { … }
null

インスタンス自体は Object である、と。

で、 Object.getPrototypeOf(sample) は Sample.prototype と同じ値です。

f:id:mslGt:20190910223738p:plain

マージしてみる

ここまでの図をまとめてみました。

f:id:mslGt:20190910223844p:plain

※オレンジ線は参照の意

こうやって見ると、クラスやクラスインスタンスからはメソッドを参照できていますが、メソッドからはほとんどつながりがなく(大元の Object しかない)、雑に引数としてメソッドを渡すと元のクラスインスタンスを見失ってしまう理由がわかるような気がします(気のせいかも)。

proto が非推奨となったり、動的に親となる Prototype が切り替わる、といった部分を考慮していなかったりと単純化してはいるはずですが、やはり複雑ではありますね。

なお、グローバルオブジェクト( Window )はこんな感じでした。

console.log(this);
console.log(Object.getPrototypeOf(this));
console.log(Object.getPrototypeOf(Object.getPrototypeOf(this)));
console.log(Object.getPrototypeOf(Object.getPrototypeOf(Object.getPrototypeOf(this))));
console.log(Object.getPrototypeOf(Object.getPrototypeOf(Object.getPrototypeOf(Object.getPrototypeOf(this)))));
console.log(Object.getPrototypeOf(Object.getPrototypeOf(Object.getPrototypeOf(Object.getPrototypeOf(Object.getPrototypeOf(this))))));
Window 
WindowPrototype { … }
WindowProperties {  }
EventTargetPrototype { addEventListener: addEventListener(), removeEventListener: removeEventListener(), dispatchEvent: dispatchEvent(), … }
Object { … }
null

f:id:mslGt:20190910223903p:plain

うーむ、ちっとも理解できた気がしない。。。

内部的なところに突っ込むともう少しわかったりするのでしょうか。。。