JavaScriptでクラスとインスタンスを実装するための2つのモデルがあります。プロトタイピングの方法とクロージャーの方法です。どちらにも長所と短所があり、さまざまな拡張バリエーションがあります。多くのプログラマーとライブラリーには、言語の醜い部分のいくつかを説明するためのさまざまなアプローチとクラス処理ユーティリティ関数があります。
その結果、混合企業では、メタクラスのミッシュマッシュがあり、すべて動作が少し異なります。さらに悪いことに、ほとんどのJavaScriptチュートリアル資料はひどいものであり、あらゆる中間点をカバーするためにある種の中間的な妥協点を提供しており、非常に混乱しています。(おそらく、作者も混乱しています。JavaScriptのオブジェクトモデルは、ほとんどのプログラミング言語とは非常に異なり、多くの場所でまっすぐに設計が不適切です。)
プロトタイプの方法から始めましょう。これは、取得できる最もJavaScriptネイティブです。最小限のオーバーヘッドコードがあり、instanceofはこの種類のオブジェクトのインスタンスで動作します。
function Shape(x, y) {
this.x= x;
this.y= y;
}
このコンストラクター関数のルックアップにメソッドをnew Shape
書き込むことで、作成されたインスタンスにメソッドを追加できprototype
ます。
Shape.prototype.toString= function() {
return 'Shape at '+this.x+', '+this.y;
};
次に、JavaScriptがサブクラス化することを呼び出すことができる限り、それをサブクラス化します。私たちはその奇妙な魔法のprototype
プロパティを完全に置き換えることによってそれを行います:
function Circle(x, y, r) {
Shape.call(this, x, y); // invoke the base class's constructor function to take co-ords
this.r= r;
}
Circle.prototype= new Shape();
メソッドを追加する前に:
Circle.prototype.toString= function() {
return 'Circular '+Shape.prototype.toString.call(this)+' with radius '+this.r;
}
この例は機能し、多くのチュートリアルでこのようなコードが表示されます。しかし、それnew Shape()
は醜いです。実際のShapeを作成する必要はありませんが、基本クラスをインスタンス化しています。JavaScriptはとてもずさんであるので、この単純なケースで動作するように起こる:それはゼロ引数が、その場合には、渡すことを可能にするx
とy
なるundefined
と、プロトタイプのに割り当てられているthis.x
とthis.y
。コンストラクター関数がより複雑なことを行っている場合、その関数は表面上で平坦になります。
したがって、基本クラスのコンストラクター関数を呼び出さずに、クラスレベルで必要なメソッドとその他のメンバーを含むプロトタイプオブジェクトを作成する方法を見つける必要があります。これを行うには、ヘルパーコードの作成を開始する必要があります。これは私が知っている最も簡単なアプローチです:
function subclassOf(base) {
_subclassOf.prototype= base.prototype;
return new _subclassOf();
}
function _subclassOf() {};
これにより、プロトタイプ内の基本クラスのメンバーが、何もしない新しいコンストラクター関数に転送され、そのコンストラクターが使用されます。これで簡単に書くことができます:
function Circle(x, y, r) {
Shape.call(this, x, y);
this.r= r;
}
Circle.prototype= subclassOf(Shape);
new Shape()
間違いの代わりに。これで、ビルドされたクラスに受け入れ可能なプリミティブのセットができました。
このモデルで検討できる改良点と拡張機能がいくつかあります。たとえば、構文糖バージョンは次のとおりです。
Function.prototype.subclass= function(base) {
var c= Function.prototype.subclass.nonconstructor;
c.prototype= base.prototype;
this.prototype= new c();
};
Function.prototype.subclass.nonconstructor= function() {};
...
function Circle(x, y, r) {
Shape.call(this, x, y);
this.r= r;
}
Circle.subclass(Shape);
どちらのバージョンにも、コンストラクター関数を継承できないという欠点があります。これは、多くの言語にあるためです。したがって、サブクラスが構築プロセスに何も追加しない場合でも、ベースが必要な引数を使用してベースコンストラクターを呼び出すことを忘れないでください。これはを使用してわずかに自動化できますapply
が、それでも書き出す必要があります。
function Point() {
Shape.apply(this, arguments);
}
Point.subclass(Shape);
したがって、一般的な拡張は、コンストラクタ自体ではなく、初期化機能を独自の関数に分割することです。この関数は、ベースから問題なく継承できます。
function Shape() { this._init.apply(this, arguments); }
Shape.prototype._init= function(x, y) {
this.x= x;
this.y= y;
};
function Point() { this._init.apply(this, arguments); }
Point.subclass(Shape);
// no need to write new initialiser for Point!
これで、各クラスに同じコンストラクタ関数のボイラープレートができました。たぶん、それを独自のヘルパー関数に移動できるので、入力を続ける必要がなくなります。たとえばFunction.prototype.subclass
、それを回転させて、基本クラスの関数にサブクラスを吐かせます。
Function.prototype.makeSubclass= function() {
function Class() {
if ('_init' in this)
this._init.apply(this, arguments);
}
Function.prototype.makeSubclass.nonconstructor.prototype= this.prototype;
Class.prototype= new Function.prototype.makeSubclass.nonconstructor();
return Class;
};
Function.prototype.makeSubclass.nonconstructor= function() {};
...
Shape= Object.makeSubclass();
Shape.prototype._init= function(x, y) {
this.x= x;
this.y= y;
};
Point= Shape.makeSubclass();
Circle= Shape.makeSubclass();
Circle.prototype._init= function(x, y, r) {
Shape.prototype._init.call(this, x, y);
this.r= r;
};
...構文は少し不格好ですが、他の言語に少し似ています。必要に応じて、いくつかの機能を追加できます。たぶん、あなたmakeSubclass
はクラス名を取って覚えて、toString
それを使ってデフォルトを提供したいでしょう。おそらく、コンストラクターがnew
演算子なしで誤って呼び出された場合にコンストラクターを検出するようにしたい場合があります(そうしないと、デバッグが非常に煩わしいことがよくあります)。
Function.prototype.makeSubclass= function() {
function Class() {
if (!(this instanceof Class))
throw('Constructor called without "new"');
...
新しいメンバーをすべて渡しmakeSubclass
てプロトタイプに追加すると、Class.prototype...
多くのことを書く必要がなくなります。多くのクラスシステムはそれを行います、例えば:
Circle= Shape.makeSubclass({
_init: function(x, y, z) {
Shape.prototype._init.call(this, x, y);
this.r= r;
},
...
});
オブジェクトシステムには望ましいと思われる多くの潜在的な機能があり、特定の数式に誰も同意しません。
閉鎖方法、そして。これにより、継承をまったく使用しないことで、JavaScriptのプロトタイプベースの継承の問題を回避します。代わりに:
function Shape(x, y) {
var that= this;
this.x= x;
this.y= y;
this.toString= function() {
return 'Shape at '+that.x+', '+that.y;
};
}
function Circle(x, y, r) {
var that= this;
Shape.call(this, x, y);
this.r= r;
var _baseToString= this.toString;
this.toString= function() {
return 'Circular '+_baseToString(that)+' with radius '+that.r;
};
};
var mycircle= new Circle();
これで、のすべてのインスタンスにShape
、toString
メソッド(およびその他のメソッドまたは追加したその他のクラスメンバー)の独自のコピーがあります。
各クラスメンバーの独自のコピーを持つすべてのインスタンスの悪い点は、効率が悪いことです。多数のサブクラス化されたインスタンスを処理している場合は、プロトタイプの継承が適している場合があります。また、ご覧のとおり、基本クラスのメソッドを呼び出すのは少し面倒です。サブクラスコンストラクターがメソッドを上書きする前に、メソッドが何であったかを覚えておかないと、失われます。
[ここにも継承がないため、instanceof
演算子は機能しません。必要な場合は、クラスを探知するための独自のメカニズムを提供する必要があります。プロトタイプ継承と同様の方法でプロトタイプオブジェクトをいじることはできますが、少しトリッキーであり、実際に機能させるだけの価値はありませんinstanceof
。]
独自のメソッドを持つすべてのインスタンスの良い点は、メソッドがそれを所有する特定のインスタンスにバインドされることです。これはthis
、メソッド呼び出しでのJavaScriptの奇妙なバインディング方法のために役立ちます。これは、メソッドをその所有者から切り離すと、次のようになります。
var ts= mycircle.toString;
alert(ts());
その場合this
、メソッドの内部は予想どおりCircleインスタンスにはなりません(実際にはグローバルwindow
オブジェクトになるため、広範囲にわたるデバッグの問題が発生します)。実際には、これは通常、メソッドが取得され、に割り当てられたsetTimeout
場合、onclick
またはEventListener
一般的に発生します。
プロトタイプの方法では、そのような割り当てごとにクロージャーを含める必要があります。
setTimeout(function() {
mycircle.move(1, 1);
}, 1000);
または、将来的に(または、Function.prototypeをハッキングする場合は)、次のように実行することもできますfunction.bind()
。
setTimeout(mycircle.move.bind(mycircle, 1, 1), 1000);
インスタンスがクロージャーの方法で行われる場合、バインディングはインスタンス変数のクロージャーによって無料で行われます(通常、that
またはと呼ばれself
ますが、個人的にはself
、JavaScriptですでに別の異なる意味を持っているので、後者に対してアドバイスします)。1, 1
ただし、上記のスニペットの引数を無料で取得することはできないため、別のクロージャーbind()
が必要になるか、それを行う必要がある場合は、
クロージャーメソッドにも多くのバリアントがあります。演算子を使用する代わりにthis
、完全に省略して、新規に作成してthat
返すことをお勧めしnew
ます。
function Shape(x, y) {
var that= {};
that.x= x;
that.y= y;
that.toString= function() {
return 'Shape at '+that.x+', '+that.y;
};
return that;
}
function Circle(x, y, r) {
var that= Shape(x, y);
that.r= r;
var _baseToString= that.toString;
that.toString= function() {
return 'Circular '+_baseToString(that)+' with radius '+r;
};
return that;
};
var mycircle= Circle(); // you can include `new` if you want but it won't do anything
どの方法が「適切」ですか?どちらも。「最高」はどれですか?それはあなたの状況に依存します。FWIW私は、OOオブジェクトを強く実行している場合は、実際のJavaScript継承のプロトタイピングを行い、単純な使い捨てページ効果のクロージャーを作成する傾向があります。
しかし、どちらの方法も、ほとんどのプログラマにとって直感に反しています。どちらも多くの乱雑な潜在的なバリエーションがあります。他の人のコード/ライブラリーを使用する場合、両方(および多くの中間および一般的に壊れたスキーム)に適合します。一般的に受け入れられている答えはありません。JavaScriptオブジェクトのすばらしい世界へようこそ。
[これは、JavaScriptが私のお気に入りのプログラミング言語ではない理由の94部です。]