20

私は数年前から、モジュール パターン風のコンストラクター パターンを使用し、通常のプロトタイプの継承なしで継承を行うことについて、人々がどう考えているのか疑問に思っていました。プログラマーが非シングルトン js クラスにモジュール パターンを使用しないのはなぜですか? 私にとっての利点は次のとおりです。

  • 非常に明確な公開範囲と非公開範囲 (コードと API を理解しやすい)
  • コールバックで $.proxy(fn, this) を介して「this」ポインターを追跡する必要はありません
  • イベントハンドラーなどで var that = this などはもうありません。「this」が表示されるたびに、コールバックに渡されているのはコンテキストであることがわかります。これは、オブジェクトインスタンスを知るために追跡しているものではありません。

短所:

  • 小さなパフォーマンス低下
  • Doug Crockford から「指を振る」可能性がありますか?

これを考慮してください(任意のjsコンソールで実行してください)

var Animal = function () {
    var publicApi = {
        Name: 'Generic',
        IsAnimal: true,
        AnimalHello: animalHello,
        GetHelloCount:getHelloCount
    };

    var helloCount = 0;

    function animalHello() {
        helloCount++;
        console.log(publicApi.Name + ' says hello (animalHello)');
    }

    function getHelloCount(callback) {
        callback.call(helloCount);
    }

    return publicApi;
};

var Sheep = function (name) {
    var publicApi = {
        Name: name || 'Woolie',
        IsSheep: true,
        SheepHello: sheepHello
    };

    function sheepHello() {
        publicApi.AnimalHello();
        publicApi.GetHelloCount(function() {
            console.log('i (' + publicApi.Name + ') have said hello ' + this + ' times (sheepHello anon callback)');
        });
    }

    publicApi = $.extend(new Animal(), publicApi);
    return publicApi;
};

var sheepie = new Sheep('Sheepie');
var lambie = new Sheep('Lambie');

sheepie.AnimalHello();
sheepie.SheepHello();
lambie.SheepHello();

私の質問は、私が見ていないこのアプローチの欠点は何ですか? これは良いアプローチですか?

ありがとう!

[アップデート]

素晴らしい反応をありがとう。みんなに賞金をあげられたらいいのに。それは私が探していたものでした。基本的に私が思ったこと。モジュールパターンを使用して、何かのインスタンスを数個以上構築することは決してありません。通常はカップルのみです。利点があると私が考える理由は、わずかなパフォーマンスの低下がコーディング エクスペリエンスのシンプルさの中で取り戻されるからです。最近は、書くべきコードがたくさんあります。また、他の人々のコードを再利用する必要があります。個人的には、プロトタイプの継承が理にかなって独断的に固執するのではなく、時間をかけて素敵なエレガントなパターンを作成してくれたことに感謝しています。

4

3 に答える 3

35

要するにパフォーマンスの問題だと思います。わずかなパフォーマンスの低下があるとおっしゃいましたが、これは実際にはアプリケーションの規模 (羊 2 匹 vs 羊 1000 匹) によって異なります。プロトタイプの継承を無視してはなりません。関数型の継承とプロトタイプの継承を組み合わせて使用​​することで、効果的なモジュール パターンを作成できます。

投稿で述べたように、JS - なぜプロトタイプを使用するのですか? 、プロトタイプの利点の 1 つは、コンストラクター内のメンバーがインスタンスごとに作成されるのに対し、プロトタイプ メンバーを 1 回だけ初期化する必要があることです。実際、新しいオブジェクトを作成しなくても、prototype に直接アクセスできます。

Array.prototype.reverse.call([1,2,3,4]);
//=> [4,3,2,1]

function add() {
    //convert arguments into array
    var arr = Array.prototype.slice.call(arguments),
        sum = 0;
    for(var i = 0; i < arr.length; i++) {
        sum += arr[i];
    }

    return sum;
}

add(1,2,3,4,5);
//=> 15

関数では、コンストラクターが呼び出されるたびに完全に新しい Animal と羊を作成するための余分なオーバーヘッドがあります。Animal.name などの一部のメンバーはインスタンスごとに作成されますが、Animal.name は静的であることがわかっているため、一度インスタンス化することをお勧めします。あなたのコードは Animal.name がすべての動物で同じであるべきであることを暗示しているので、プロトタイプに移動した場合は Animal.prototype.name を更新するだけで、すべてのインスタンスの Animal.name を簡単に更新できます。

このことを考慮

var animals = [];
for(var i = 0; i < 1000; i++) {
    animals.push(new Animal());
}

機能継承・モジュールパターン

function Animal() {

    return {
      name : 'Generic',
      updateName : function(name) {
          this.name = name;
      }
   }

}


//update all animal names which should be the same
for(var i = 0;i < animals.length; i++) {
    animals[i].updateName('NewName'); //1000 invocations !
}

対プロトタイプ

Animal.prototype = {
name: 'Generic',
updateName : function(name) {
   this.name = name
};
//update all animal names which should be the same
Animal.prototype.updateName('NewName'); //executed only once :)

上記のように、現在のモジュール パターンでは、すべてのメンバーに共通する必要があるプロパティを更新する効率が失われます。

可視性について懸念がある場合は、現在プライベートメンバーをカプセル化するために使用しているのと同じモジュラーメソッドを使用しますが、 到達する必要がある場合は、これらのメンバーにアクセスするために特権メンバーも使用します。特権メンバーは、プライベート変数にアクセスするためのインターフェイスを提供するパブリック メンバーです。最後に、共通メンバーをプロトタイプに追加します。

もちろん、このルートをたどるには、これを追跡する必要があります。あなたの実装には本当です

  • コールバックで $.proxy(fn, this) を介して「this」ポインターを追跡する必要はありません
  • イベントハンドラーなどで var that = this などはもうありません。「this」が表示されるたびに、コールバックに渡されているのはコンテキストであることがわかります。これは、オブジェクトインスタンスを知るために追跡しているものではありません。

、しかし、毎回非常に大きなオブジェクトを作成しているため、プロトタイプの継承を使用する場合と比較して、より多くのメモリを消費します。

類推としてのイベント委任

プロトタイプを使用してパフォーマンスを向上させることは、DOM を操作するときにイベント委任を使用してパフォーマンスを向上させることに例えられます。JavaScript でのイベント委任

大規模な食料品リストがあるとしましょう。Yum.

<ul ="grocery-list"> 
    <li>Broccoli</li>
    <li>Milk</li>
    <li>Cheese</li>
    <li>Oreos</li>
    <li>Carrots</li>
    <li>Beef</li>
    <li>Chicken</li>
    <li>Ice Cream</li>
    <li>Pizza</li>
    <li>Apple Pie</li>
</ul>

クリックしたアイテムをログに記録したいとしましょう。実装の 1 つは、すべての item(bad)にイベント ハンドラーをアタッチすることですが、リストが非常に長い場合、管理するイベントが多くなります。

var list = document.getElementById('grocery-list'),
 groceries = list.getElementsByTagName('LI');
//bad esp. when there are too many list elements
for(var i = 0; i < groceries.length; i++) {
    groceries[i].onclick = function() {
        console.log(this.innerHTML);
    }
}

もう 1 つの実装は、1 つのイベント ハンドラーを親 (良い) にアタッチし、その 1 つの親にすべてのクリックを処理させることです。ご覧のとおり、これは一般的な機能のプロトタイプを使用することに似ており、パフォーマンスが大幅に向上します

//one event handler to manage child elements
 list.onclick = function(e) {
   var target = e.target || e.srcElement;
   if(target.tagName = 'LI') {
       console.log(target.innerHTML);
   }
}

機能継承とプロトタイプ継承の組み合わせによる書き換え

機能継承とプロトタイプ継承の組み合わせがわかりやすく書けると思います。上記の手法を使用してコードを書き直しました。

var Animal = function () {

    var helloCount = 0;
    var self = this;
    //priviledge methods
    this.AnimalHello = function() {
        helloCount++;
        console.log(self.Name + ' says hello (animalHello)');
    };

    this.GetHelloCount = function (callback) {
        callback.call(null, helloCount);
    }

};

Animal.prototype = {
    Name: 'Generic',
    IsAnimal: true
};

var Sheep = function (name) {

    var sheep = new Animal();
    //use parasitic inheritance to extend sheep
    //http://www.crockford.com/javascript/inheritance.html
    sheep.Name = name || 'Woolie'
    sheep.SheepHello = function() {
        this.AnimalHello();
        var self = this;
        this.GetHelloCount(function(count) {
            console.log('i (' + self.Name + ') have said hello ' + count + ' times (sheepHello anon callback)');
        });
    }

    return sheep;

};

Sheep.prototype = new Animal();
Sheep.prototype.isSheep = true;

var sheepie = new Sheep('Sheepie');
var lambie = new Sheep('Lambie');

sheepie.AnimalHello();
sheepie.SheepHello();
lambie.SheepHello();

結論

要点は、パフォーマンスと可視性の両方の問題に取り組むために、プロトタイプと機能の継承の両方を利点として使用することです。最後に、小規模な JavaScript アプリケーションで作業していて、これらのパフォーマンスの問題が問題にならない場合、この方法は実行可能なアプローチになります。

于 2013-05-24T03:52:00.497 に答える
2

モジュールパターン風のコンストラクターパターン

これは、寄生的継承または機能的継承として知られています。

私にとっての利点は次のとおりです。

  • 非常に明確な公開範囲と非公開範囲 (このコードと API を理解しやすい)

同じことが、従来のコンストラクター パターンにも当てはまります。ところで、現在のコードでは、 と が非公開であるかどうかは明確animalHellogetHelloCountはありません。それを気にするなら、エクスポートされたオブジェクトリテラルでそれらを正しく定義する方が良いかもしれません。

  • コールバックで $.proxy(fn, this) を介して「this」ポインターを追跡する必要はありません
  • イベントハンドラーなどで var that = this などはもうありません。「this」が表示されるたびに、コールバックに渡されているのはコンテキストであることがわかります。これは、オブジェクトインスタンスを知るために追跡しているものではありません。

それは基本的に同じです。thatこの問題を解決するには、逆参照またはバインドを使用します。オブジェクトの「メソッド」をコールバックとして直接使用する状況は非常にまれであり、コンテキストとは別に、追加の引数をフィードしたい場合が多いため、これが大きな欠点とは思いません。ところで、thatコードでも参照を使用していますpublicApi。そこで呼び出されます。

プログラマーが非シングルトン js クラスにモジュール パターンを使用しないのはなぜですか?

さて、あなたはすでにいくつかの欠点を挙げています。さらに、すべての利点 (シンプルさ、ダイナミズムなど) とともに、プロトタイプの継承が失われていますinstanceof。もちろん、それらが適用されない場合もあり、ファクトリ関数はまったく問題ありません。これらの場合に実際に使用されます。

publicApi = $.extend(new Animal(), publicApi);
…
… new Sheep('Sheepie');

コードのこれらの部分も少し混乱します。ここで変数を別のオブジェクトで上書きしていますが、それはコードの途中で発生します。これを「宣言」と見なし (ここで親プロパティを継承しています!)、関数の先頭に配置することをお勧めします。var publicApi = $.extend(Animal(), {…});

また、newここではキーワードを使用しないでください。関数をコンストラクターとして使用せず、継承するインスタンスを作成したくないAnimal.prototype(実行速度が低下する)。また、そのコンストラクターの呼び出しからプロトタイプの継承を期待する人々を混乱させますnew。わかりやすくするために、関数の名前をmakeAnimalおよびに変更することもできますmakeSheep

nodejsでこれに匹敵するアプローチは何ですか?

この設計パターンは、完全に環境に依存しません。Node.js でも、クライアントや他のすべての EcmaScript 実装と同じように機能します。そのいくつかの側面は、言語に依存しません。

于 2013-05-23T23:18:39.110 に答える
2

あなたのアプローチでは、関数をオーバーライドしてスーパー関数を便利に呼び出すことはできません。

function foo ()
{
}

foo.prototype.GetValue = function ()
{
        return 1;
}


function Bar ()
{
}

Bar.prototype = new foo();
Bar.prototype.GetValue = function ()
{
    return 2 + foo.prototype.GetValue.apply(this, arguments);
}

また、プロトタイプ アプローチでは、オブジェクトのすべてのインスタンス間でデータを共有できます。

function foo ()
{
}
//shared data object is shared among all instance of foo.
foo.prototype.sharedData = {
}

var a = new foo();
var b = new foo();
console.log(a.sharedData === b.sharedData); //returns true
a.sharedData.value = 1;
console.log(b.sharedData.value); //returns 1

プロトタイプ アプローチのもう 1 つの利点は、メモリを節約できることです。

function foo ()
{
}

foo.prototype.GetValue = function ()
{
   return 1;
}

var a = new foo();
var b = new foo();
console.log(a.GetValue === b.GetValue); //returns true

一方、あなたのアプローチでは、

var a = new Animal();
var b = new Animal();
console.log(a.AnimalHello === b.AnimalHello) //returns false

これは、新しいオブジェクトごとに関数の新しいインスタンスが作成され、プロトタイプ アプローチの場合はすべてのオブジェクト間で共有されることを意味します。これは、少数のインスタンスでは大きな違いはありませんが、多数のインスタンスが作成されると、かなりの違いが見られます。

また、プロトタイプのもう 1 つの強力な機能は、すべてのオブジェクトが作成された後でも、すべてのオブジェクトのプロパティを一度に変更できることです (オブジェクトの作成後に変更されていない場合のみ)。

function foo ()
{
}
foo.prototype.name = "world";

var a = new foo ();
var b = new foo ();
var c = new foo();
c.name = "bar";

foo.prototype.name = "hello";

console.log(a.name); //returns 'hello'
console.log(b.name); //returns 'hello'
console.log(c.name); //returns 'bar' since has been altered after object creation

結論: プロトタイプ アプローチの上記の利点がアプリケーションにとってあまり役に立たない場合は、そのアプローチの方が優れています。

于 2013-05-25T07:27:52.043 に答える