web-dev-qa-db-ja.com

古典的な継承よりもプロトタイプの継承の利点は?

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

263
Pierreten

私はこの答えが3年遅れていることを知っていますが、現在の答えは プロトタイプ継承が従来の継承よりも優れている方法 についての十分な情報を提供していないと本当に思います。

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

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

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

プロトタイプ継承の問題は、JavaScriptの観点から説明されていることだと思います。 JavaScriptは大好きですが、JavaScriptのプロトタイプ継承は間違っています。古典的な継承とは異なり、プロトタイプの継承には2つのパターンがあります。

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

残念ながら、JavaScriptはプロトタイプ継承のコンストラクターパターンを使用します。これは、JavaScriptが作成されたときに、 Brendan Eich (JSの作成者)が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);

実際、次のように、これらすべてを単一のオブジェクトリテラルに結合できます。

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のコンストラクターパターンを避けるべき理由は他にもたくさんあります。私のブログ投稿でそれらについて読むことができます: Constructors vs Prototypes


それでは、古典的継承に対するプロトタイプ継承の利点は何ですか?最も一般的な引数をもう一度見て、whyを説明しましょう。

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

CMS 彼の答えの状態:

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

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

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

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

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

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

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

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

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

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

静的型は単なるメタデータであることもわかっています。これらは、プログラマーとコンパイラーという2種類の読者を対象とした特別な種類のコメントです。静的型は、おそらく両方の読者グループがプログラムの意図を理解するのを助けるために、計算に関するストーリーを伝えます。ただし、静的型は実行時に破棄される可能性があります。これは、最終的には定型化されたコメントに過ぎないためです。彼らは血統の書類のようなものです。それはある種の不安な性格のタイプを彼らの犬に対してより幸せにするかもしれませんが、犬は確かに気にしません。

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

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

古典的な背景から来たほとんどのプログラマーは、古典的な継承はプロトタイプの継承よりも強力であると主張しています:

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

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

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

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

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

もちろん、多くのプログラマーは instanceofisPrototypeOf を別の言い方をするため、これを真の継承とは見なしません。ただし、これは、連結を介してプロトタイプから継承するすべてのオブジェクトにプロトタイプの配列を格納することにより、簡単に修正できます。

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

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

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

ただし、JavaScriptでは、継承するプロパティやプロトタイプを正確に制御できるため、ダイヤモンドの問題を耳にすることはほとんどないでしょう。

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

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

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

Javaはこの動作で有名です。 Bob NystromPratt Parsers に関する彼のブログ投稿で次の逸話に言及したことをはっきりと覚えています。

ここでは、Javaの「4重に署名してください」レベルの官僚主義を愛する必要があります。

繰り返しますが、それはJavaがひどく吸い込むからだと思います。

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

プロトタイプ継承は多重継承を許可するため、多重継承を必要とするコードは、従来の継承を持ちながら多重継承を持たない言語ではなく、プロトタイプ継承を使用して記述した場合、冗長性が低くなります。

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

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

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

結論

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

JavaScriptを正しく教える必要があります。つまり、コンストラクターパターンの代わりにプロトタイプパターンを使用してコードを記述する方法を新しいプログラマーに示す必要があります。

プロトタイプパターンを使用してプロトタイプ継承を説明するのが簡単になるだけでなく、プログラマーの質も向上します。

この回答が気に入ったら、「 Why Prototypal Inheritance Matters 」に関するブログ投稿も読んでください。私を信じてください、あなたは失望しません。

530
Aadit M Shah

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

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

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

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

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

上記の行を読んで、従来のクラス/オブジェクトスキームの長所と短所を理解できると思います。もちろん、各エリアにはもっとたくさんあるので、残りは答える人に任せます。

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

言語のプロトタイプの性質は、古典的に訓練された人々を混乱させる可能性がありますが、実際にはこれはであることがわかりますreallyシンプルで強力なコンセプト、 微分継承

classificationにする必要はありません。コードは小さく、冗長性が低く、オブジェクトは他のより一般的なオブジェクトを継承します。

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

プロトタイプの継承は近い将来、より一般的になります。 ECMAScript 5th Edition 仕様では Object.create メソッドが導入され、別のインスタンスを継承する新しいオブジェクトインスタンスを作成できます本当に簡単な方法で:

var obj = Object.create(baseInstance);

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

28
CMS

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"

ご覧のとおり、古典的なスタイルで宣言された「クラス」のプロトタイプを操作することは可能であるため、プロトタイプ継承を使用する利点はありません。これは、古典的な方法のサブセットです。

10
Noel Abrahams

Web開発:プロトタイプ継承と古典的継承

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

クラシカルVsプロトタイプ継承-スタックオーバーフロー

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

2
ratty