JavaScriptのクラスと継承


JavaScriptはこれだけ広く使われている言語にしては、意外と今回のような情報は見つからなかったりします。

まずはおさらい。

  • 全てはオブジェクトである。
  • 継承はプロトタイプチェーンによって行う。
  • 関数名にnewをつけて呼び出すと、その関数はコンストラクタと呼ばれ、新たなオブジェクトを作る。

このへんでつまづく人はいないと思うのです。

でも、具体的にBaseというクラスをDerivedというクラスが継承し、そのオブジェクトをnewで作ったという場合に、プロトタイプチェーンがどのように構築されるかをはっきり説明できる人は少ないような気がします。
文章でだらだら説明しても必ずわかりにくくなるので、結論から図で描いてしまうとこうなっているわけです。

コンストラクタとクラスオブジェクトは別物で、 constructor と prototype というフィールドで相互参照しているという点が初心者にはわかりにくいと思います。
なお、 __proto__ というフィールドは内部的なプロトタイプチェーンを実装するための参照であり、コードから直接アクセスしてはいけません

このような継承関係が構築されていることは、下記のようなコードで確認できます。

class Base{
}

class Derived extends Base{
}

const base = new Base()
const derived = new Derived()

console.log(derived.__proto__.__proto__ === base.__proto__)
console.log(base.constructor === Base)
console.log(derived.constructor === Derived)
console.log(Derived.prototype.__proto__ === Base.prototype)
console.log(Base.prototype.__proto__ === Object.prototype)
console.log(Derived.__proto__ === Base)
console.log(Base.__proto__ === Function.__proto__)

console.log(function(){}.__proto__ === Function.prototype)
console.log(function(){} instanceof Function)

ES5以前

ES5以前は、 Java 風の class 構文はありませんでしたので、次のような書き方をよくしました。

Derived.prototype = new Base()

しかし、実はこれでは前述の継承関係を構築するには不十分で、プロトタイプからコンストラクタへの constructor フィールドによる参照が構築されません。相互参照すべきところが片参照になっています。
これを正しく実現するには、古いブラウザへの互換性も考慮して以下のような関数を定義する必要がありました。

/// Custom inheritance function that prevents the super class's constructor
/// from being called on inehritance.
/// Also assigns constructor property of the subclass properly.
function inherit(subclass,base){
    // If the browser or ECMAScript supports Object.create, use it
    // (but don't forget to redirect constructor pointer to subclass)
    if(Object.create){
        subclass.prototype = Object.create(base.prototype);
    }
    else{
        var sub = function(){};
        sub.prototype = base.prototype;
        subclass.prototype = new sub;
    }
    subclass.prototype.constructor = subclass;
}

使い方はこんな感じです。

function Base(){
}
function Derived(){
    Base.call(this)
}
inherit(Derived, Base)

まあ、面倒ですよね。
ES6で class 構文が導入されたのはいいことですが、なんで最初から用意してくれなかったんだと言いたくもなります。これは関数型の継承とプロトタイプベースの継承を好みに応じて選べるようにするためではないかと私は考えています。

なぜこうなってしまったのか

コンストラクタとプロトタイプ・オブジェクトが異なるのはなぜなのでしょうか。考えてみればわかりますが、コンストラクタはあくまでも関数なので、プロトタイプに Function を持ち、 callapply といったメソッドを継承しています。新しく定義するクラスは、もちろん関数のお仲間とは限りませんので、プロトタイプチェインに Function を含めたくはないですよね。

オブジェクト・モデルを考えればこのような仕様になることは納得できなくもないですが、 C++ や Java のオブジェクト指向に慣れた人には馴染むのが難しいのではないでしょうか。まあ、 Brendan Eich は半意識的にこのようにした1ようですが。


  1. JavaScript 20years という論文でそこらへんの詳細が振り返られています。 JavaScript は10日で最初のバージョンが書かれたという伝説についても、本人が実際のところを明かしている興味深い読み物です。 https://dl.acm.org/doi/10.1145/3386327