Help us understand the problem. What is going on with this article?

JavaScriptのクラスと継承

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

まずはおさらい。

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

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

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

prototype (3).png

コンストラクタとクラスオブジェクトは別物で、 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 

msakuta
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした