少し前に、私はすでに長い間私が興味を持っているトピックを手に入れました。 このトピックはJavaScriptの継承です。
このテーマに関するネットワークには多くの記事がありますが、その完全性とロジックで満足できる一般的な分析は見つかりませんでした。 なぜ一般化分析を見つけたいのですか? 実際、JSでのオブジェクト指向プログラミングの特別な複雑さは、(少なくとも私にとっては)衝撃的な(可能な限りの)実装の多様性にあります。 かなり長い間失敗した検索の後、私はこの問題を自分で解決しようとすることにしました。
JavaScriptでOOPを深く理解しているふりをしていないこと、そしてOOP全般を深く理解しているふりをしていないことをすぐに言いたいと思います。 私の分析の試みが誰かに役立つなら嬉しいでしょうが、ある意味では、出版の主な目的は反対です-私はそれを自分のために明確にするためにトピックで私よりも優れている人々のコメントを利用したいと思います。
一般的な推論
JSは非常に強力で柔軟な言語であると考えられています。 一方、JSは、控えめに言っても、言語は未完成であるという意見があります。 さて、OOPに関しては、JavaScriptはできることをすべて示しています-両方の点で。
このパラドックスから、特に、各javascriptには動機があり、JSのオブジェクトの継承に対する
最初は、すべての決定をクラスベースとプロトタイプ (「クラスレス」)の2つの大きなグループに分割できると計算しました。 JavaScript自体が古典的なOOPスタイルとプロトタイプスタイルの間で「ぶら下がっている」ように思われることを考えると、このような区分は自然に見えました。 しかし、ほとんどの場合、JavaScriptのOOPはプロトタイプ言語による古典的なOOPの単なる複製であるため、このような分類を拒否することにしました。
ご質問
代わりに、次の3つの質問を使用することにしました。これらの質問は、JSの継承に目を向けるのに役立つようです。
1)クラスの実装方法
2)インスタンスがクラスのプロパティを取得する方法
3)クラス継承はどのように実装されますか?
(注:以下、プロパティとは、オブジェクトのプロパティとメソッドの両方を意味します。実際、プロトタイププログラミングには「スロット」という用語がありますが、JSのこのコンテキストではほとんど使用されません)
どうやってクラスを組織しますか?
この質問には根本的に異なる2つの答えがあります。
- クラスは、コンストラクター関数を使用して編成されます。
同時に、new演算子を使用してインスタンスオブジェクトが作成され、コンストラクター自体がJSスタイルで、複数のロール(関数、クラス、オブジェクトコンストラクターの両方)を一度に結合します。 - クラスは、オブジェクトファクトリの機能を使用して編成されます。
この場合、インスタンスはファクトリー関数によって直接返されます。
コンストラクターを使用してクラスを編成する最も単純な例は、次のようになります。
// 1 - ( ) function Class() { this.value = 'some value'; // this.some_method = function() { // console.log('some_method invoked'); } } var obj1 = new Class(); // var obj2 = new Class(); //
オブジェクトファクトリを使用して編成された同様のクラスは次のとおりです。
// 2 - ( ) function Class() { var obj = {}; obj.value = 'some value'; // obj.some_method = function () { // console.log('some_method invoked'); } return obj; } var obj1 = Class(); // ( , new) var obj2 = Class(); //
(特別なキーワードnewの存在は、それが言語の主流であるコンストラクター関数であることを示唆していることに気付くかもしれません)
プロパティを保存する方法は?
2番目の質問に移ります。インスタンスがクラスのプロパティをどのように取得するかです。
ここでは、まず、「クラスプロパティの取得」の意味を明確にする必要があります。 私たちは、オブジェクトがクラスで定義されたプロパティとメソッドを自動的に「所有する」と信じていました。 この「所有」の事実は、与えられたものとして認識します。 しかし、JSではそうではありません。 ここで、オブジェクトが(疑似)クラスからプロパティを受け取る方法を選択できます 。 再び2つの可能性があります。オブジェクトがプロトタイプでクラスのプロパティを取得するか、オブジェクトに直接含まれます。
最初のオプションについては、見たところです。前の2つの例では、コンストラクター関数(例1)およびオブジェクトファクトリ(例2)のインスタンスにプロパティの直接格納を実装しています。
プロトタイプからプロパティを取得する方法はどのようなものですか? そして、ここに方法があります:
// 3 - -, function Class() { } Class.prototype.value = 'some value'; // Class.prototype.some_method = function() { // console.log('some_method invoked'); } var obj1 = new Class(); // var obj2 = new Class(); //
この例のコンストラクター関数は完全に空であり、すべてのプロパティとメソッドがプロトタイプに設定されていることに注意してください。 もちろん、実際には、コンストラクターはおそらく有用なもの(たとえば、初期パラメーターの設定)に使用されます。
ご覧のとおり、プロトタイプはデザイナーと自然に組み合わされています。 工場設備はどうですか? 標準に従って、オブジェクトのプロトタイプはそのコンストラクター関数を使用して設定されるため、状況はより複雑です。 つまり、コンストラクターを使用しない場合、オブジェクトのプロトタイプを操作することはできません。 そのため、このトリックを使用する必要があります。
// 4 - , // , // , . function derivate(o) { function F() {} F.prototype = o; return new F(); } function Class() { return derivate(Class.obj); } // , Class.obj = {}; Class.obj.value = 'some value'; // Class.obj.some_method = function () { // console.log('some_method invoked'); } var obj1 = Class(); // var obj2 = Class(); //
この例は、前の例よりも少し複雑です。 この複雑さに何かポイントはありますか? 少なくとも、この例は、derivate()関数が言語のプロトタイプ機能を正確に拡張するという点で興味深い(まあ、またはこれらの機能の欠如を補う)。 一方、クラスの作成を担当するコードが前の例の類似のコードと非常に類似していることが判明したのは興味深いことです。 実際、組み込みのClass.prototypeの代わりに、独自のプロパティClass.objを作成しましたが、前の例でJavaScriptが提供していたことを行う必要がありました。 それらのすべての類似点について、2つの例は本質的に完全に異なることに注意してください。
インスタンスプロトタイプにクラスプロパティを配置する必要があるのはなぜですか? クラスのすべてのインスタンスの同じ名前のプロパティがメモリ内の同じ場所を物理的に占有するため、少なくとも、この方法でリソースを節約できます。
コンストラクター。 どのように継承しますか?
最後に、最も興味深いことになります。 3番目の問題は継承です。
基本的に、プロトタイプ言語では、コピーまたは委任という2つの方法のいずれかでプロパティの継承を実装できます。
JavaScript自体は委任による継承を実装しています。つまり、これはつまり、オブジェクトの欠落しているプロパティがそのプロトタイプのプロパティの中から検索されることを意味します。
コピー継承は、 プロトン連結の概念と一致しています 。 この場合、親オブジェクトのプロパティは単に子オブジェクトにコピーされます。 プロトタイプの連結はJavaScriptの一部ではありませんが、実装できないという意味ではありません。
呼び出し/適用のソリューション
コピー継承を実装する方法を見てみましょう。 理論的には、このために補助関数copyMethodsを作成できます。
// , function copyMethods(from, to) { for(m in from) { // if (typeof from[m] != "function") continue; // , to[m] = from[m]; // } }
ただし、applyメソッドで小さなフォーカスを使用すれば、はるかに簡単にできます。
// 5 - + + // // function Class(val) { // this.value = val; this.getName = function() { return "Class(id = " + this.getId() + ")"; } this.getId = function() { return 1; } } function SubClass() { // , : // copyMethods(new Class(arguments[0]), this); // : Class.apply(this, arguments); // Class() this var super_getName = this.getName; // , this.getName = function() { return "SubClass(id = " + this.getId() + ") extends " + super_getName.call(this); } this.getId = function() { return 2; } } // o = new SubClass(5); console.log(o.value); // 5 console.log(o.getName()); // "SubClass(id = 2) extends Class (id = 2)" console.log(o instanceof Class); // false [ instanceof]
この例は、3つの提案されたすべての質問に答える最初の例であり、実際には既成のソリューションであるため、前の例よりもやや簡潔にしました。 彼は例1で始めたことを完了します。
この決定の完全性を考えると、長所と短所を評価することは理にかなっています。 だから
なぜそれが良いのですか:
-簡潔でシンプルな実装
-メソッドの各定義でクラス名を繰り返す必要はありません( DRY原則 )
-デザイナーの自動継承
-プログラムのグローバルコンテキストには影響しません(グローバル補助機能などはありません)
-プライベートプロパティの実装が簡単
なぜこれが悪いのですか:
-メモリは効率的に使用されません(すべてのオブジェクトのすべての同じプロパティがコピーとして保存されます)
-親メソッドを呼び出すのは便利ではありません
-instanceof演算子と互換性がありません
-ネイティブクラスとの互換性はありません(Dateなどの組み込みクラスの子孫は作成できません)
ほぼ標準的なアプローチ
もちろん、例1(コンストラクター+インスタンス内のクラスプロパティ)は、継承の方法に関する質問に対する2番目のバージョンの回答に従って、異なる方向に開発することができます。
// 6 - + + // // function Class(val) { // this.value = val; this.getName = function() { return "Class(id = " + this.getId() + ")"; } this.getId = function() { return 1; } } function SubClass() { var super_getName = this.getName; // this.getName = function() { return "SubClass(id = " + this.getId() + ") extends " + super_getName.call(this); } this.getId = function() { return 2; } } SubClass.prototype = new Class(); // // o = new SubClass(5); console.log(o.value); // undefined [ ] console.log(o.getName()); // "SubClass(id = 2) extends Class (id = 2)" console.log(o instanceof Class); // true [ instanceof]
子クラスは、初期パラメーターの設定方法を知らないことに注意してください。 前のアプローチと比較して、このアプローチには利点がありますか? さて、プロトタイプによる継承はより多くのメモリを消費します。 一方、このソリューションは明らかに中途半端です-各インスタンスのクラスプロパティを互いに独立して作成します。
非常に標準的なアプローチ
最後に、おそらくJavaScriptの標準として考えられたソリューションに到達します。 3つの質問の観点から見た彼の「式」は、コンストラクター+プロトタイプのクラスプロパティ+委任継承のように見えます。
// 7 - + + // // function Class(val) { this.init(val); } Class.prototype.init = function (val) { this.value = val; } Class.prototype.value = 5; // Class.prototype.getName = function() { return "Class(id = " + this.getId() + ")"; } Class.prototype.getId = function() { return 1; } function SubClass(val) { this.init(val); } SubClass.prototype = new Class(); // SubClass.prototype.class_getName = SubClass.prototype.getName; // SubClass.prototype.getName = function() { return "SubClass(id = " + SubClass.prototype.getId() + ") extends " + SubClass.prototype.class_getName(); } SubClass.prototype.getId = function() { return 2; } function SubSubClass(val) { this.init(val); } SubSubClass.prototype = new SubClass(); SubSubClass.prototype.subclass_getName = SubSubClass.prototype.getName; // SubClass.prototype.getName = function() { return "SubSubClass(id = " + SubSubClass.prototype.getId() + ") extends " + SubSubClass.prototype.subclass_getName(); } // o = new SubSubClass(5); console.log(o.value); // 5 console.log(o.getName()); // "SubSubClass(id = 2) extends SubClass(id = 2) extends Class(id = 2))" console.log(o instanceof Class); // true [ instanceof]
この例の不便さを強調したかったので、3つのクラス全体を説明しました。 この例全体は、同じ名前、同じ冗長構成を繰り返すことで構成されているようです。 しかし、これは十分ではありません。プロトタイプ継承のメソッド自体には根本的な非効率性が含まれています。階層を記述するために、プロトタイプオブジェクトを作成して初期化する (つまり、initメソッドを呼び出す)必要があります。 皮肉化はリソースを集中的に使用する可能性があるため、これは悪いことです。
奇妙なことに、欠点にもかかわらず、このアプローチは非常に人気があります。 なんで? 実際、ここには謎はありません。メソッドの欠点はあらゆる種類のアドオンで補われますが、その「ネイティブ」な言語の組み込み構文はアドオンでは提供できません。
さて、コンストラクターを持つ4つのソリューションの最後のオプションがあります:コンストラクター+プロトタイプのプロパティ+コピーによる継承。 風に逆らってプログラムすることの意味を自分で体験したい人のための独立した演習として残します;)そのようなアプローチは理論的に可能であり、時には使用されることさえあります(いずれにせよ、ネットワーク上でそれを満たしました)が、それは非常に不便です(呼び出し/適用での使用は不可能であり、プロトタイプに保存した後にコピーしてプロパティを継承することは論理的ではありません)
このように、オブジェクトのファクトリーについて-続編で、デザイナーに対処しました。
更新:残念ながら、記事の最初の部分を書いてから1年以上経った後、私は2番目の部分の時間がないことを認めざるを得ません。 すべての関係者に謝ります:)