原型継承が古典よりも優れている点は?


271

それで、私はついにこれらの年の間ずっと足を引きずることをやめて、JavaScriptを「適切に」学ぶことを決めました。言語設計の最も頭を悩ます要素の1つは、継承の実装です。Rubyの経験があったので、クロージャーと動的型付けを見て本当に幸せでした。しかし、私の人生では、継承のために他のインスタンスを使用するオブジェクトインスタンスからどのようなメリットが得られるかを理解できません。



回答:


560

この回答は3年遅れであることは知っていますが、現在の回答では、プロトタイプの継承がクラシックの継承よりも優れていることについて十分な情報が得られないと思います。

最初に、JavaScriptプログラマーがプロトタイプ継承を擁護して述べる最も一般的な引数を見てみましょう(これらの引数は、現在の回答のプールから取っています)。

  1. それは簡単です。
  2. それは強力です。
  3. これにより、コードが小さくなり、冗長性が低下します。
  4. これは動的であるため、動的言語に適しています。

現在、これらの議論はすべて有効ですが、誰もその理由を説明することに煩わされていません。数学を学ぶことが重要だと子供に言うようなものです。確かにそうですが、子供は確かに気にしません。そして、それが重要だと言って数学のような子供を作ることはできません。

プロトタイプ継承の問題は、JavaScriptの観点から説明されていることだと思います。私はJavaScriptを愛していますが、JavaScriptのプロトタイプ継承は間違っています。従来の継承とは異なり、プロトタイプ継承には2つのパターンがあります。

  1. プロトタイプ継承のプロトタイプパターン。
  2. プロトタイプ継承のコンストラクタパターン。

残念ながらJavaScriptはプロトタイプ継承のコンストラクタパターンを使用しています。これは、JavaScriptが作成されたときに、JSの作成者であるBrendan EichがそれをJava(従来の継承を持つ)のように見せたかったためです。

そして、Visual Basicのような補完言語は、当時のMicrosoftの言語ファミリのC ++であったため、Javaの弟としてそれを推進していました。

JavaScriptでコンストラクターを使用する場合、他のコンストラクターから継承するコンストラクターを考えるため、これは悪いことです。これは間違っています。プロトタイプの継承では、オブジェクトは他のオブジェクトから継承します。コンストラクターは決して登場しません。これはほとんどの人を混乱させるものです。

伝統的な継承を持つJavaのような言語の人々は、コンストラクターはクラスのように見えますが、クラスのようには動作しないため、さらに混乱します。ダグラス・クロックフォードは、次のように述べました。

この間接参照は、古典的に訓練されたプログラマにとって言語をより親しみやすくするためのものでしたが、JavaプログラマがJavaScriptを持っているという非常に低い意見からわかるように、そうすることはできませんでした。JavaScriptのコンストラクターパターンは、古典的な人々には魅力的ではありませんでした。また、JavaScriptの真のプロトタイプの性質も覆い隠しました。その結果、言語を効果的に使用する方法を知っているプログラマーはほとんどいません。

そこにあります。馬の口から真っ直ぐに。

真のプロトタイプの継承

プロトタイプの継承はすべてオブジェクトに関するものです。オブジェクトは他のオブジェクトからプロパティを継承します。これですべてです。プロトタイプ継承を使用してオブジェクトを作成するには、2つの方法があります。

  1. 新しいオブジェクトを作成します。
  2. 既存のオブジェクトを複製して拡張します。

注: JavaScriptでは、オブジェクトのクローンを作成する2つの方法(委任連結)が提供されています。以降では、「クローン」という語を委任による継承のみに言及するために使用し、「コピー」という語を連結による継承に排他的に言及するために使用します。

十分な話。いくつかの例を見てみましょう。半径の円があるとしましょう5

var circle = {
    radius: 5
};

半径から円の面積と円周を計算できます:

circle.area = function () {
    var radius = this.radius;
    return Math.PI * radius * radius;
};

circle.circumference = function () {
    return 2 * Math.PI * this.radius;
};

次に、別の半径の円を作成します10。これを行う1つの方法は次のとおりです。

var circle2 = {
    radius: 10,
    area: circle.area,
    circumference: circle.circumference
};

ただし、JavaScriptはより良い方法委譲を提供します。Object.create関数は、これを実行するために使用されます。

var circle2 = Object.create(circle);
circle2.radius = 10;

それで全部です。JavaScriptでプロトタイプの継承を行いました。簡単ではなかったですか?オブジェクトを取得し、それを複製し、必要なものをすべて変更し、プレスト-自分で新しいオブジェクトを取得します。

「これは簡単ですか?新しい円を作成したいときはいつでも、クローンを作成circleして手動で半径を割り当てる必要があります」と尋ねるかもしれません。解決策は、関数を使用して重い作業を行うことです。

function createCircle(radius) {
    var newCircle = Object.create(circle);
    newCircle.radius = radius;
    return newCircle;
}

var circle2 = createCircle(10);

実際、次のように、これらすべてを1つのオブジェクトリテラルに組み合わせることができます。

var circle = {
    radius: 5,
    create: function (radius) {
        var circle = Object.create(this);
        circle.radius = radius;
        return circle;
    },
    area: function () {
        var radius = this.radius;
        return Math.PI * radius * radius;
    },
    circumference: function () {
        return 2 * Math.PI * this.radius;
    }
};

var circle2 = circle.create(10);

JavaScriptでのプロトタイプの継承

上記のプログラムで気付いた場合、create関数はのクローンを作成し、それにcircle新しいradiusを割り当ててから返します。これは、JavaScriptでのコンストラクターの動作とまったく同じです。

function Circle(radius) {
    this.radius = radius;
}

Circle.prototype.area = function () {
    var radius = this.radius;
    return Math.PI * radius * radius;
};

Circle.prototype.circumference = function () {         
    return 2 * Math.PI * this.radius;
};

var circle = new Circle(5);
var circle2 = new Circle(10);

JavaScriptのコンストラクターパターンは、逆のプロトタイプパターンです。オブジェクトを作成する代わりに、コンストラクタを作成します。newキーワードは、バインドthisのクローンにコンストラクタの内部ポインタをprototypeコンストラクタの。

混乱しているように聞こえますか?これは、JavaScriptのコンストラクタパターンが不必要に複雑になっているためです。これは、ほとんどのプログラマが理解するのが難しいと感じていることです。

他のオブジェクトから継承するオブジェクトについて考える代わりに、他のコンストラクタから継承するコンストラクタについて考え、完全に混乱します。

JavaScriptのコンストラクターパターンを回避する必要がある理由は他にもたくさんあります。これらについては、私のブログ投稿(コンストラクタ対プロトタイプ)で読むことができます。


それでは、古典的継承よりもプロトタイプ継承の利点は何ですか?最も一般的な議論をもう一度見て、その理由を説明しましょう。

1.プロトタイプの継承は簡単です

CMSは彼の答えで述べています:

私の意見では、プロトタイプ継承の主な利点はその単純さです。

今行ったことを考えてみましょう。circle半径がのオブジェクトを作成しました5。次に、クローンを作成し、クローンに半径を指定しました10

したがって、プロトタイプの継承を機能させるために必要なものは2つだけです。

  1. 新しいオブジェクト(オブジェクトリテラルなど)を作成する方法。
  2. 既存のオブジェクトを拡張する方法(例:)Object.create

対照的に、古典的な継承ははるかに複雑です。古典的な継承では、次のようになります。

  1. クラス。
  2. オブジェクト。
  3. インターフェース。
  4. 抽象クラス。
  5. 最終クラス。
  6. 仮想基本クラス。
  7. コンストラクタ。
  8. デストラクタ。

あなたはアイデアを得ます。ポイントは、プロトタイプの継承が理解しやすく、実装しやすく、推論しやすいことです。

Steve Yeggeが彼の古典的なブログ投稿「Portrait of a N00b」でそれを述べているように:

メタデータは、何か別の種類の説明またはモデルです。コード内のコメントは、計算の自然言語による説明にすぎません。メタデータをメタデータにするのは、厳密には必要ないということです。血統書類のある犬を飼っていて、書類を紛失しても、完全に有効な犬がいます。

同じ意味で、クラスは単なるメタデータです。クラスは継承に厳密には必要ありません。ただし、一部の人々(通常はn00bs)は、クラスを使用するほうが快適であると感じています。それは彼らに誤った安心感を与えます。

まあ、私たちは静的型が単なるメタデータであることも知っています。これらは、プログラマとコンパイラの2種類の読者を対象とした特別な種類のコメントです。静的型は、おそらく両方の読者グループがプログラムの意図を理解するのに役立つように、計算についてのストーリーを伝えます。しかし、静的な型は実行時に捨てることができます。結局のところ、静的な型はコメントを様式化したものだからです。彼らは血統の書類のようなものです。それは彼らの犬についてある種の不安定な性格タイプを幸せにするかもしれませんが、犬は確かに気にしません。

先に述べたように、クラスは人々に誤った安心感を与えます。たとえばNullPointerException、コードが完全に読みやすい場合でも、Javaでが多すぎます。私は古典的な継承が通常プログラミングの邪魔になると思いますが、多分それは単なるJavaです。Pythonには驚くべき古典的な継承システムがあります。

2.プロトタイプの継承は強力です

古典的なバックグラウンドを持つ出身のほとんどのプログラマーは、古典的な継承はプロトタイプの継承よりも強力であると主張しています。

  1. プライベート変数。
  2. 多重継承。

この主張は誤りです。JavaScript がクロージャーを介しプライベート変数をサポートすることはすでに知っていますが、多重継承についてはどうでしょうか?JavaScriptのオブジェクトにはプロトタイプが1つだけあります。

真実は、プロトタイプの継承が複数のプロトタイプからの継承をサポートしていることです。プロトタイプの継承とは、あるオブジェクトが別のオブジェクトから継承することを意味します。プロトタイプ継承を実装するには、実際には2つの方法があります

  1. 委任または差分継承
  2. クローニングまたは連結継承

はいJavaScriptでは、オブジェクトは他の1つのオブジェクトにのみ委任できます。ただし、任意の数のオブジェクトのプロパティをコピーできます。たとえば_.extendこれだけです。

もちろん、多くのプログラマーはこれを真の継承であるinstanceofとは考えていませんisPrototypeOf。ただし、これは、連結によってプロトタイプから継承するすべてのオブジェクトにプロトタイプの配列を格納することで簡単に修正できます。

function copyOf(object, prototype) {
    var prototypes = object.prototypes;
    var prototypeOf = Object.isPrototypeOf;
    return prototypes.indexOf(prototype) >= 0 ||
        prototypes.some(prototypeOf, prototype);
}

したがって、プロトタイプの継承は、古典的な継承と同じくらい強力です。実際、プロトタイプの継承では、コピーするプロパティと異なるプロトタイプから除外するプロパティを手動で選択できるため、従来の継承よりもはるかに強力です。

従来の継承では、継承するプロパティを選択することは不可能(または少なくとも非常に困難)です。ダイヤモンドの問題を解決するために仮想基本クラスとインターフェースを使用しています。

ただし、JavaScriptでは、どのプロパティを継承し、どのプロトタイプから継承するかを正確に制御できるため、ダイヤモンドの問題を聞くことはほとんどありません。

3.プロトタイプの継承は冗長性が低い

古典的な継承は必ずしもより冗長なコードにつながるわけではないので、この点を説明するのは少し難しいです。実際、古典的であれプロトタイプ的であれ、継承はコードの冗長性を減らすために使用されます。

1つの議論は、古典的な継承を持つほとんどのプログラミング言語は静的に型付けされ、ユーザーが明示的に型を宣言する必要があるということかもしれません(暗黙の静的型付けを持つHas​​kellとは異なります)。したがって、これはより冗長なコードにつながります。

Javaはこの動作で悪名高いです。私ははっきりと覚えているボブ・ナイストロームをについての彼のブログ記事で、次の逸話に言及プラットパーサ

ここでは、Javaの「四重に署名してください」というレベルの官僚機構が好きです。

繰り返しになりますが、それは、Javaが非常に不便だからです。

有効な引数の1つは、従来の継承を持つすべての言語が多重継承をサポートしているわけではないということです。ここでもJavaが思い浮かびます。はいJavaにはインターフェースがありますが、それだけでは不十分です。時には、本当に多重継承が必要な場合があります。

プロトタイプの継承では複数の継承が可能であるため、古典的な継承はあるが複数の継承はない言語ではなく、プロトタイプの継承を使用して記述した場合、複数の継承を必要とするコードはそれほど冗長ではありません。

4.プロトタイプの継承は動的です

プロトタイプ継承の最も重要な利点の1つは、プロトタイプを作成した後に、プロトタイプに新しいプロパティを追加できることです。これにより、プロトタイプに委譲するすべてのオブジェクトが自動的に使用できるようになるプロトタイプに新しいメソッドを追加できます。

一度クラスを作成すると、実行時にそれを変更できないため、これは従来の継承では不可能です。これはおそらく、古典的継承に対するプロトタイプ継承の最大の利点の1つであり、それが最上位であるはずです。しかし、私は最後のために最高のものを保存するのが好きです。

結論

プロトタイプの継承が重要です。JavaScriptプログラマーに、プロトタイプ継承のプロトタイプパターンを優先して、プロトタイプ継承のコンストラクターパターンを放棄する理由を説明することが重要です。

JavaScriptを正しく教えることから始める必要があります。つまり、コンストラクターパターンではなくプロトタイプパターンを使用してコードを記述する方法を新しいプログラマーに示す必要があります。

プロトタイプのパターンを使用してプロトタイプの継承を説明するのが簡単になるだけでなく、より優れたプログラマーになるでしょう。

この回答が気に入ったら、「プロトタイプの継承が重要である理由」に関する私のブログ投稿も読んでください。私を信じて、あなたは失望することはありません。


33
私はあなたがどこから来たのか理解しており、プロトタイプ継承は非常に有用であることに同意しますが、「プロトタイプ継承はクラシック継承よりも優れている」と仮定すると、すでに失敗する運命にあります。いくつかの視点を与えるために、私のライブラリjTypesは、JavaScriptの古典的な継承ライブラリです。だから、それを作るのに時間をかけた男なので、私はまだここに座って、プロトタイプ継承が素晴らしくて非常に便利だと言います。しかし、それはプログラマーが持っている多くのツールの1つにすぎません。同様に、プロトタイプの継承にも多くの欠点があります。
レッドライン2013年

7
私はあなたが言ったことに完全に同意します。あまりにも多くのプログラマーが、JavaScriptの古典的な継承の欠如を拒否したり、JavaScriptを単純でばかげた言語として非難したりしているように感じます。それは実際には非常に強力な概念であると私は同意し、多くのプログラマーはそれを受け入れ、それを学ぶべきです。そうは言っても、JavaScriptの開発者の多くは、JavaScriptでの伝統的な継承のすべてに反対しているため、実際には引数の根拠がないのです。どちらもそれ自体が同等に強力で、同様に有用です。
レッドライン2013年

9
まあそれはあなたの意見ですが、私は同意しません。CoffeeScriptやTypeScriptなどの人気の高まりは、適切なときにこの機能を利用したい開発者の大きなコミュニティがあることを示していると思います。あなたが言ったように、ES6は構文糖を追加しますが、それでもjTypesの拡張性を提供しません。余談ですが、私はあなたの反対票に対して責任を負うものではありません。私はあなたに反対しますが、あなたが悪い答えを持っているとは思えません。あなたはとても徹底的でした。
レッドライン

25
あなたは単語クローンをたくさん使っていますが、それは単に間違っています。Object.create指定されたプロトタイプで新しいオブジェクトを作成しています。あなたが選んだ言葉は、プロトタイプが複製されているような印象を与えます。
Pavel Horal 2013年

7
@Aadit:それほど防御する必要はありません。あなたの答えは非常に詳細で投票に値します。「リンクされた」は「クローン」のドロップイン置換である必要があるとは示唆していませんでしたが、「クローン」という用語の独自の定義を主張するかどうかにかかわらず、オブジェクトとそれが継承するプロトタイプの間の接続をより適切に説明しています" か否か。それを変更するか、変更しないでください。それは完全にあなたの選択です。
アンディE

42

実際に質問にインラインで回答させてください。

プロトタイプの継承には、次の長所があります。

  1. 継承は環境と同じくらい動的であるため、動的言語に適しています(JavaScriptへの適用性はここで明らかです)。これにより、大量のインフラストラクチャコードを使用せずに、クラスをカスタマイズするように、その場ですばやく作業を行うことができます。 。
  2. 古典的なクラス/オブジェクト二分法スキームよりもプロトタイピングオブジェクトスキームを実装する方が簡単です。
  3. これにより、「メタクラス」(メタクラスは好きになりませんでした...申し訳ありません!)や「固有値」などのオブジェクトモデルの周りの複雑な鋭いエッジの必要がなくなります。

ただし、次の欠点があります。

  1. プロトタイプ言語の型チェックは不可能ではありませんが、非常に困難です。プロトタイプ言語のほとんどの「型チェック」は、純粋な実行時の「ダック型」スタイルのチェックです。これはすべての環境に適しているわけではありません。
  2. 同様に、静的(または、多くの場合は動的)分析によってメソッドディスパッチを最適化するなどの処理を行うことは困難です。それ(私は強調します:できる)非常に非効率的に非常に簡単にできます。
  3. 同様に、オブジェクトの作成は、プロトタイプ言語では、従来のクラス/オブジェクトの二分法よりもはるかに遅くなる可能性があります(通常は遅くなります)。

上記の行の間を読んで、従来のクラス/オブジェクトスキームの対応する長所と短所を考え出すことができると思います。もちろん、各エリアにはもっと多くのエリアがあるので、残りは他の人に答えてもらいます。


1
ちょっと見て、ファンボーイではない簡潔な答え。これが質問のトップアンサーであることを本当に願っています。
Siliconrockstar

今日、コードが実行されている間にコードをコンパイルして、セクションごとに異なるコードを作成できる動的ジャストインタイムコンパイラーがあります。JavaScriptは実際には、古典的なクラスを使用するRubyまたはPythonよりも高速です。これは、プロトタイプを使用していても、それを最適化するために多くの作業が行われているためです。
aoeu256

28

IMOのプロトタイプ継承の主な利点は、その単純さです。

言語のプロトタイプの性質は、古典的に訓練された人々を混乱させる可能性がありますが、実際には、これは非常に単純で強力な概念である差分継承であることがわかります。

分類を行う必要はありません。コードは小さく、冗長性が低く、オブジェクトは他のより一般的なオブジェクトから継承されます。

プロトタイプで考えると、クラスが必要ないことにすぐに気付くでしょう...

近い将来、プロトタイプの継承がより一般的になるでしょう。ECMAScript5th Edition仕様でObject.createメソッドが導入されました。これにより、別のインスタンスから継承する新しいオブジェクトインスタンスを非常に簡単な方法で作成できます。

var obj = Object.create(baseInstance);

この新しいバージョンの標準はすべてのブラウザーベンダーによって実装されており、より純粋なプロトタイプの継承が見られるようになると思います...


11
「あなたのコードはより小さく、より冗長ではありません...」、なぜですか?私は「差分継承」のウィキペディアリンクを確認しましたが、これらの主張をサポートするものは何もありません。なぜ伝統的な継承はより大きく、より冗長なコードをもたらすのでしょうか?
Noel Abrahams

4
まさに、ノエルに同意します。プロトタイプの継承は、仕事を成し遂げるための1つの方法にすぎませんが、それは正しい方法ではありません。さまざまなツールは、さまざまなジョブに対してさまざまな方法で実行されます。プロトタイプの継承はその場所を持っています。それは非常に強力でシンプルなコンセプトです。そうは言っても、真のカプセル化と多態性に対するサポートの欠如は、JavaScriptを重大な不利な立場に置きます。これらの方法論はJavaScriptに比べてはるかに古く、原理的には健全です。したがって、プロトタイプが「より良い」と考えることは、まったく間違った考え方です。
レッドライン

1
プロトタイプベースの継承を使用してクラスベースの継承をシミュレートできますが、その逆はできません。それは良い議論かもしれません。また、カプセル化は言語機能よりも慣例として捉えています(通常、リフレクションによってカプセル化を解除できます)。ポリモーフィズムに関して-メソッドの引数をチェックするときに、単純な "if"条件を記述する必要はありません(そして、コンパイル中にターゲットメソッドが解決されると、少し速度が向上します)。ここには実際のJavaScriptの欠点はありません。
Pavel Horal 2013年

IMO、プロトタイプは素晴らしいです。Haskellのような関数型言語を作成することを考えています...しかし、抽象化を作成する代わりに、すべてをプロトタイプに基づいて作成します。合計を一般化する代わりに、階乗を「階乗」するために、合計関数のプロトタイプを作成し、+を*に、0を1に置き換えて積を作成する必要があります。「モナド」をプロミス/コールバックの「then」を「then」の同義語であるflatMapに置き換えるプロトタイプとして説明します。プロトタイプは関数型プログラミングを大衆にもたらすのに役立つと思います。
aoeu256

11

2つの方法のどちらを選択するかはそれほど多くありません。把握すべき基本的な考え方は、JavaScriptエンジンが読み取るオブジェクトのプロパティを与えられると、最初にインスタンスをチェックし、そのプロパティがない場合はプロトタイプチェーンをチェックするということです。次に、プロトタイプとクラシックの違いを示す例を示します。

プロトタイプ

var single = { status: "Single" },
    princeWilliam = Object.create(single),
    cliffRichard = Object.create(single);

console.log(Object.keys(princeWilliam).length); // 0
console.log(Object.keys(cliffRichard).length); // 0

// Marriage event occurs
princeWilliam.status = "Married";

console.log(Object.keys(princeWilliam).length); // 1 (New instance property)
console.log(Object.keys(cliffRichard).length); // 0 (Still refers to prototype)

インスタンスメソッドを使用したクラシック (各インスタンスは独自のプロパティを格納するため非効率

function Single() {
    this.status = "Single";
}

var princeWilliam = new Single(),
    cliffRichard = new Single();

console.log(Object.keys(princeWilliam).length); // 1
console.log(Object.keys(cliffRichard).length); // 1

効率的なクラシック

function Single() {
}

Single.prototype.status = "Single";

var princeWilliam = new Single(),
    cliffRichard = new Single();

princeWilliam.status = "Married";

console.log(Object.keys(princeWilliam).length); // 1
console.log(Object.keys(cliffRichard).length); // 0
console.log(cliffRichard.status); // "Single"

ご覧のように、クラシックスタイルで宣言された「クラス」のプロトタイプを操作することが可能であるため、プロトタイプ継承を使用しても実際にはメリットはありません。それは古典的な方法のサブセットです。


2
このトピックに関する他の回答とリソースを見ると、答えは「プロトタイプの継承は、JavaScriptに追加された構文糖のサブセットであり、古典的な継承の外観を可能にする」と述べているように見えます。OPは、JavaScript内のインスタンス化手法を比較するのではなく、JSのプロトタイプ継承が他の言語の従来の継承よりも優れている点について尋ねているようです。
フェーダーダーク

2

Web開発:プロトタイプの継承と従来の継承

http://chamnapchhorn.blogspot.com/2009/05/prototypal-inheritance-vs-classical.html

クラシックとプロトタイプの継承-スタックオーバーフロー

古典的な対プロトタイプの継承


20
別のSOリンクでない限り、リンクを貼り付けるよりも、リンクの内容を要約する方がよいと思います(私自身が以前行ったものです)。これは、リンク/サイトがダウンし、質問への回答が失われ、検索結果に影響を与える可能性があるためです。
James Westgate、

最初のリンクは、なぜプロトタイプ継承が続くのという質問に答えていませんか?それは単にそれを説明します。
viebel 2012年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.