6時だョ!!全員集合!!

Rails・JavaScrictを中心にアウトプットします。

2022年1月20日 JavaScript (JS Primer) プロパティの参照とプロトタイプチェーン

プロトタイプオブジェクトのプロパティがどのようにインスタンスから参照されるか

オブジェクトのプロパティを参照するときに、オブジェクト自身がプロパティを持っていない場合でも、そこで探索が終わるわけではありません。 オブジェクトのPrototype内部プロパティ(仕様上の内部的なプロパティ)の参照先であるプロトタイプオブジェクトに対しても探索を続けます。 これは、スコープに指定した識別子の変数がなかった場合に外側のスコープへと探索するスコープチェーンと良く似た仕組みです。

つまり、オブジェクトがプロパティを探索するときは次のような順番で、それぞれのオブジェクトを調べます。 すべてのオブジェクトにおいて見つからなかった場合の結果はundefinedを返します。

クラスのインスタンスを出力してみる

オブジェクトがプロパティを探索するときは次のような順番で調べられます。

1.instanceオブジェクト自身
2. instanceオブジェクトの[[Prototype]]の参照先(プロトタイプオブジェクト)
3.どこにもなかった場合はundefined

上記の例の元のコードは以下です。

class ConflictClass {
    constructor() {
        this.method = () => {
            console.log("インスタンスオブジェクトのメソッド");
        };
    }
    method() {
        console.log("プロトタイプメソッド");
    }
}
const conflict = new ConflictClass();
console.log(conflict);

出力結果: { method: [Function] }

instanceオブジェクト自身 instanceオブジェクトのPrototypeの参照先(プロトタイプオブジェクト) どこにもなかった場合はundefined 次のコードでは、インスタンスオブジェクト自身はmethodプロパティを持っていません。 そのため、実際に参照しているのはクラスのプロトタイプオブジェクトのmethodプロパティです。

ここを読んで感動しました。 スコープに指定した識別子の変数がなかった場合に外側のスコープへと探索するスコープチェーンと良く似た仕組みです。 私はプロトタイプチェーンという言葉に違和感を覚えていましたが、ここを読んで完全に腑に落ちました。インスタンス化した変数からメソッドを呼び出そうとした場合にまず今いるスコープからインスタンスオブジェクトのmethodを探索します。もしなかった場合にスコープチェーンの仕組みの外のスコープに探しにいく原理と同様に、prototypeに探索しにいきます。(ここでチェーンという名称の意味が繋がりました)そして、もしprototypeにもmethodがなかった場合にundefinedになります。これで同じクラス内にプロトタイプメソッドとインスタンスオブジェクトのメソッドを定義していたら、インスタンスメソッドが優先される理由も完全に理解できました。

class MyClass {
    method() {
        console.log("プロトタイプのメソッド");
    }
}
const instance = new MyClass();
// インスタンスには`method`プロパティがないため、プロトタイプオブジェクトの`method`が参照される
instance.method(); // "プロトタイプのメソッド"
// `instance.method`の参照はプロトタイプオブジェクトの`method`と一致する
const Prototype = Object.getPrototypeOf(instance);
console.log(instance.method === Prototype.method); // => true
実行

このように、インスタンスオブジェクトにmethodが定義されていなくても、クラスのプロトタイプオブジェクトのmethodを呼び出すことができます。 このプロパティを参照する際に、オブジェクト自身からPrototype内部プロパティへと順番に探す仕組みのことをプロトタイプチェーンと呼びます。

プロトタイプチェーンの仕組みを疑似的なコードとして表現すると次のような動きをしています。

// プロトタイプチェーンの動作の疑似的なコード
class MyClass {
    method() {
        console.log("プロトタイプのメソッド");
    }
}
const instance = new MyClass();
// `instance.method()`を実行する場合
// 次のような呼び出し処理が行われている
// インスタンス自身が`method`プロパティを持っている場合
if (instance.hasOwnProperty("method")) {
    instance.method();
} else {
    // インスタンスの`[[Prototype]]`の参照先(`MyClass`のプロトタイプオブジェクト)を取り出す
    const prototypeObject = Object.getPrototypeOf(instance);
    // プロトタイプオブジェクトが`method`プロパティを持っている場合
    if (prototypeObject.hasOwnProperty("method")) {
        // `this`はインスタンス自身を指定して呼び出す
        prototypeObject.method.call(instance);
    }
}

プロトタイプチェーンの仕組みによって、プロトタイプオブジェクトに定義したプロトタイプメソッドをインスタンスから呼び出せます。

普段は、プロトタイプオブジェクトやプロトタイプチェーンといった仕組みを意識する必要はありません。 class構文はこのようなプロトタイプを意識せずにクラスを利用できるように導入された構文です。 しかし、プロトタイプベースである言語のJavaScriptではクラスをこのようなプロトタイプを使って表現していることは知っておくとよいでしょう。

参考

JS primer プロパティの参照とプロトタイプチェーン