プロトタイプ継承は古典的継承よりも優れているか?質問する

プロトタイプ継承は古典的継承よりも優れているか?質問する

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

ベストアンサー1

注:この回答を10年前に書いて以来、私は継承が有害であることを学びました。継承を使用する正当な理由はありませんソフトウェア開発において、継承でできることはすべて、継承でより良くできるのです。構成使用を検討してください代数データ型その代わり。


この回答は 3 年遅れであることは承知していますが、現在の回答ではプロトタイプ継承が古典的な継承よりも優れている理由について十分な情報が提供されていないと思います。

まず、プロトタイプ継承を擁護するために JavaScript プログラマーが述べる最も一般的な議論を見てみましょう (これらの議論は、現在の回答のプールから抜粋したものです)。

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

これらの議論はすべて正当なものですが、誰もその理由を説明しようとはしません。これは、子供に数学の勉強が大切だと教えるようなものです。確かに重要ですが、子供は気にしません。そして、数学が大切だと言っても、子供に数学を好きにさせることはできません。

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

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

残念ながらJavaScriptはプロトタイプ継承のコンストラクタパターンを使用しています。これはJavaScriptが作られたときに、ブレンダン・アイク(JS の作成者は) それを Java (古典的な継承を持つ) のように見えるようにしたいと考えました。

そして、私たちはそれを、当時の Microsoft の言語ファミリーにおける Visual Basic が 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新しい を割り当ててそれを返すことに気付くでしょう。これはまさに JavaScript のコンストラクターが行うことです。radius

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バインドします。thisprototype

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

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


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

1. プロトタイプ継承はシンプル

コンテンツ管理システム彼は答えの中でこう述べている。

私の意見では、プロトタイプ継承の主な利点はそのシンプルさにあります。

先ほど行ったことを考えてみましょう。circle半径 のオブジェクトを作成しました5。次に、そのオブジェクトを複製し、複製に半径 を与えました10

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

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

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

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

要点は、プロトタイプ継承の方が理解しやすく、実装しやすく、推論しやすいということです。

スティーブ・イェッジは彼の古典的なブログ記事でこう述べています。「初心者のポートレート「:」

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

同じ意味で、クラスは単なるメタデータです。継承にはクラスが厳密に必要というわけではありません。しかし、一部の人々 (通常は初心者) は、クラスの方が扱いやすいと感じています。クラスは彼らに誤った安心感を与えます。

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

先ほど述べたように、クラスは人々に誤った安心感を与えます。たとえば、NullPointerExceptionJava では、コードが完全に判読可能であるにもかかわらず、s が多すぎます。私は、古典的な継承がプログラミングの邪魔になることが多いと感じていますが、それは Java だけなのかもしれません。Python には、素晴らしい古典的な継承システムがあります。

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

古典的な背景を持つほとんどのプログラマーは、古典的な継承はプロトタイプ継承よりも強力であると主張します。その理由は次のとおりです。

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

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

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

  1. 委任または差別的継承
  2. クローンまたは連結継承

はい、JavaScriptではオブジェクトを他のオブジェクトに委譲することしかできません。ただし、任意の数のオブジェクトのプロパティをコピーすることは可能です。たとえば、_.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 つの議論としては、古典的な継承を持つほとんどのプログラミング言語は静的に型付けされており、ユーザーが明示的に型を宣言する必要がある (暗黙の静的型付けを持つ Haskell とは異なります) ということが挙げられます。したがって、これによりコードがより冗長になります。

Javaはこの動作で有名です。私ははっきりと覚えていますボブ・ニストロム彼はブログ記事の中で次のような逸話を述べている。プラットパーサー:

Java の「4 部署名してください」というレベルの官僚主義は、本当にすごいです。

繰り返しますが、それは Java があまりにもひどいからだと思います。

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

プロトタイプ継承では多重継承が可能なので、多重継承を必要とするコードは、従来の継承はあるが多重継承がない言語で記述するよりも、プロトタイプ継承を使用して記述した方が冗長性が低くなります。

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

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

これは、クラスが一度作成されると実行時に変更できないため、従来の継承では不可能です。これはおそらく、従来の継承に対するプロトタイプ継承の最大の利点であり、最初に述べるべきでした。ただし、私は最良の点は最後に残しておくのが好きです。

結論

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

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

プロトタイプ パターンを使用すると、プロトタイプ継承の説明が容易になるだけでなく、より優れたプログラマーも育成されます。

おすすめ記事