1. ホーム
  2. ジャバスクリプト

[解決済み】プロトタイプ継承の利点は古典的なものよりも?

2022-04-15 20:50:46

質問

この数年、足を引っ張っていたのをやめ、ついにJavaScriptを「ちゃんと」勉強することにしました。 この言語の設計で最も頭を悩ませる要素の1つは、継承の実装です。 Rubyの経験があるので、クロージャや動的型付けを見たときは本当に嬉しかったのですが、オブジェクトのインスタンスが他のインスタンスを使って継承することでどんな利点があるのか、どうしても理解できないのです。

どうすれば解決するの?

この回答は3年遅れていますが、現在の回答は、以下の点について十分な情報を提供していないと思います。 プロトタイプ継承は古典的な継承より優れているのか? .

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

  1. シンプルだから。
  2. パワフルである。
  3. より小さく、より冗長なコードになります。
  4. 動的であるため、動的言語に適している。

これらの主張はすべて正しいのですが、誰もその理由をわざわざ説明してくれません。子供に「数学の勉強は大事だよ」と言っているようなものです。確かに重要ですが、子どもはまったく気にしていませんし、重要だと言って子どもを数学好きにすることはできません。

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

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

残念ながら、JavaScriptはプロトタイプ継承のコンストラクタ・パターンを採用しています。というのも、JavaScriptが作られた当時は ブレンダン・アイク (JSの生みの親)は、(古典的な継承を持つ)Javaのように見えるようにしたかったのです。

<ブロッククオート

そして、当時のマイクロソフトの言語ファミリーの中で、Visual BasicがC++を補完するような言語として、Javaの弟分として推していたのです。

JavaScriptでコンストラクタを使うとき、コンストラクタが他のコンストラクタを継承していると考えるので、これはまずい。これは間違っています。プロトタイプ継承では、オブジェクトは他のオブジェクトから継承されます。コンストラクタはその中に入ってきません。これが多くの人を混乱させる原因です。

Javaなど古典的な継承を行う言語から来た人は、さらに混乱する。コンストラクタはクラスのように見えるが、クラスのように振る舞わないからだ。というのも ダグラス・クロックフォード と述べています。

この間接性は、古典的な訓練を受けたプログラマーにとって、より親しみやすい言語に見せるためのものでしたが、JavaプログラマーがJavaScriptに対して非常に低い評価をしていることからもわかるように、それは失敗に終わりました。JavaScriptのコンストラクタ・パターンは古典的な人々には魅力的ではありませんでした。また、JavaScriptの真のプロトタイプ的な性質も曖昧にしてしまいました。その結果、この言語の効果的な使い方を知っているプログラマは非常に少なくなってしまったのです。

その通りです。馬の口から直接

真のプロトタイプ継承

プロトタイピング継承は、オブジェクトに関するすべてのことです。オブジェクトは他のオブジェクトからプロパティを継承します。それだけです。プロトタイピング継承を利用してオブジェクトを作成する方法は2つあります。

  1. 全く新しいオブジェクトを作成する。
  2. 既存のオブジェクトをクローンして拡張する。

注意 JavaScript では、オブジェクトのクローンを作成する方法が 2 つあります。 デリゲーション 連結 . 今後、委譲による継承を指す場合はclone"、連結による継承を指す場合はcopy"と呼ぶことにします。

話はもういい。では、例を見てみましょう。例えば、半径 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;
};

次に、もう1つの半径の円を作成したいと思います。 10 . 一つの方法として、次のようなものがあります。

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は、彼の古典的なブログ記事「"」でこう言っている。 N00b の肖像 "です。

<ブロッククオート

メタデータとは、何か他のものの説明やモデルのようなものです。あなたのコードにあるコメントは、計算の自然言語による記述に過ぎません。メタデータがメタデータたる所以は、それが厳密には必要でないことです。もし私が血統書付の犬を飼っていて、その血統書を紛失しても、私は完全に有効な犬を飼っていることになります。

それと同じ意味で、クラスはメタデータに過ぎません。クラスは厳密には継承のために必要なものではありません。しかし、人によっては(たいてい初心者は)クラスの方が扱いやすいと感じることがあります。それは、彼らに間違った安心感を与えるからです。

<ブロッククオート

まあ、静的型は単なるメタデータであることも分かっていますしね。これは、プログラマーとコンパイラーという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. プロトタイプの継承は冗長性が少ない

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

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

Javaはこの動作で悪名高いです。私ははっきりと覚えています。 ボブ・ナイストロム に関する彼のブログ記事で、次のような逸話を紹介しています。 プラット・パーサー :

Java の「4倍で署名してください」レベルの官僚主義が好きなんだろうな。

繰り返しになりますが、それはJavaがあまりにクソだからに他なりません。

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

プロトタイピング継承は多重継承を可能にするので、多重継承が必要なコードは、古典的な継承を持ちながら多重継承ができない言語よりも、プロトタイピング継承を使って書いた方が冗長にならない。

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

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

古典的な継承では、一度作ったクラスは実行時に変更できないので、このようなことはできません。これはおそらく、古典的な継承に対するプロトタイピング継承の唯一最大の利点であり、一番上にあるべきものです。しかし、私はベストを最後に残しておきたいのです。

まとめ

プロトタイプ継承は重要です。JavaScriptプログラマに、なぜプロトタイプ継承のコンストラクタ・パターンを捨てて、プロトタイプ継承のプロトタイプ・パターンを支持するのかを教育することが重要です。

私たちはJavaScriptを正しく教え始める必要があり、それはつまり、新しいプログラマに、コンストラクタ・パターンの代わりにプロトタイパターンを使ってコードを書く方法を示すことを意味します。

プロトタイプ継承をプロトタイプ・パターンで説明することは、より簡単であるだけでなく、より良いプログラマーを作ることにもつながります。

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