web-dev-qa-db-ja.com

$ .extendとモジュールパターンを使用した単純なJavaScript継承

私はここ数年、人々がモジュールパターン風のコンストラクタパターンで継承を行い、通常のプロトタイプ継承なしで継承を行うことについて何を考えているのか疑問に思いました。プログラマーが非シングルトンの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();

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

ありがとう!

[更新]

素晴らしい反応をありがとう。みんなに賞金をあげたいです。それは私が探していたものでした。基本的に私が思ったこと。モジュールパターンを使用して、いくつかのインスタンスを構築することはありません。通常はカップルのみ。それがその利点を持っていると私が思う理由は、あなたが見るどんな小さなパフォーマンスの低下も、コーディング経験の単純さで再捕獲されることです。最近書けるコードはたくさんあります。また、他の人のコードを再利用する必要もあります。個人的には、プロトタイプの継承を理にかなったものに固執するのではなく、誰かが時間をかけて素敵なエレガントなパターンを作成したときに感謝しています。

20
Ron Gilchrist

結局、それはパフォーマンスの問題に帰着すると思います。小さなパフォーマンスの低下があるとおっしゃいましたが、これは実際にはアプリケーションの規模(2羊vs 1000羊)に依存します。 プロトタイプの継承は無視してはいけませんそして機能とプロトタイプの継承の混合を使用して効果的なモジュールパターンを作成できます。

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

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.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 :)

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

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

もちろん、このルートに行くには、これを追跡する必要があります。実際の実装では、

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

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

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

プロトタイプを使用してパフォーマンスを得るanalogyは、DOMを操作するときにevent delegationを使用することによってパフォーマンスが向上します。 JavaScriptでのイベントの委任

大きな買い物リストがあるとしましょう。

<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つの実装はすべてのアイテムにイベントハンドラーをアタッチする(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つのイベントハンドラーをparent(good)にアタッチし、その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アプリケーションでこれらのパフォーマンスの問題が心配されていないに取り組んでいる場合、このメソッドは実行可能なアプローチになります。

35
AnthonyS

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

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

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

2
Parthik Gosar

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

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

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

  • 非常に明確なパブリックおよびプライベートスコープ(このコードとAPIを理解しやすい)

同じことが、古典的なコンストラクタパターンにも当てはまります。ところで、現在のコードでは、animalHellogetHelloCountがプライベートであるかどうかは明確ではありません。エクスポートされたオブジェクトリテラルでそれらを正しく定義することは、それが気になる場合に適しています。

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

基本的には同じです。この問題を解決するには、that逆参照orバインディングを使用します。また、オブジェクトの「メソッド」を直接コールバックとして使用する状況は非常にまれであり、コンテキストとは別に、追加の引数を提供したい場合が多いので、これは大きな欠点とは思いません。ところで、コードでは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実装と同じように機能します。そのいくつかの側面は言語に依存しません。

2
Bergi