使用$ .extend和模块模式的简单javascript继承

时间:2013-05-20 22:24:31

标签: javascript jquery inheritance module

我想知道几年后人们如何看待使用模块模式 - esque构造函数模式继承并且没有正常的原型继承。为什么程序员不使用模块模式用于非单例js类?对我来说,优点是:

  • 非常清晰的公共和私人范围(易于理解代码和API)
  • 无需在回调中通过$ .proxy(fn,this)跟踪'this'指针
  • 使用事件处理程序等等,不再有var = 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();

我的问题是我没有看到这种方法的缺点是什么?这是一个好方法吗?

谢谢!

[更新]

感谢您的回复。希望我能给予每个人赏金。这就是我在寻找的东西。基本上我的想法。我绝不会使用模块模式来构造多个实例。通常只有一对。我认为它有其优点的原因是你看到的任何小的性能退化都会在编码体验的简单性中重新获得。这些天我们写了很多代码。我们还必须重用其他人的代码,我个人感谢有人花时间创造一个漂亮优雅的模式,而不是在有意义的时候教条地坚持原型继承。

3 个答案:

答案 0 :(得分:35)

我认为归结为性能问题。您提到性能下降很小,但这实际上取决于应用程序的规模(2只绵羊对1000只绵羊)。 原型继承不应该被忽略,我们可以使用功能和原型继承的混合创建一个有效的模块模式。

如帖子JS - Why use 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.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 !
}

VS。原型

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

如上图所示,对于您目前的模块模式,我们会失去效率 更新应该与所有成员共同的属性。

如果您对可见性感到怀疑,我会使用您当前使用的相同模块化方法来封装私有成员但也使用  如果需要访问这些成员,请访问priviledged members特权成员是提供访问私有变量的接口的公共成员。最后添加普通成员到原型。

当然要走这条路,你需要跟踪这个。  确实,在你的实现中有

  • 无需在回调中通过$ .proxy(fn,this)跟踪'this'指针
  • 不再使用事件处理程序等等= =等等。每当我看到'this'时,我知道它是被传递给回调的上下文,它不是我跟踪以了解我的对象实例的东西。

,但是每次创建一个非常大的对象,与使用一些原型继承相比,消耗更多内存

事件代表团为类比

类比通过使用原型获得性能,通过在操作DOM时使用事件委派来提高性能。Event Delegation in 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>

假设您要记录您点击的项目。 一个实现是将事件处理程序附加到每个项目(坏),但如果我们的列表很长,则会有很多要管理的事件。

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);
    }
}

另一个实现是将一个事件处理程序附加到父(好)并让一个父处理所有点击。 正如您所看到的,这类似于将原型用于常见功能并显着提高性能

//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应用程序并且这些性能问题不是问题, 那么你的方法将是可行的方法。

答案 1 :(得分:2)

  

模块模式 - esque构造函数模式

这称为寄生继承功能继承

  

对我而言,优点是:

     
      
  • 非常清晰的公共和私人范围(易于理解此代码和api)
  •   

对于经典构造函数模式也是如此。顺便说一下,在你当前的代码中,animalHellogetHelloCount是否应该是私有的并不是非常清楚。如果您关心它,可能会更好地在导出的对象文字中定义它们。

  
      
  • 无需在回调中通过$ .proxy(fn,this)跟踪'this'指针
  •   
  • 使用事件处理程序等等,不再有var = this等等。每当我看到'this'时,我知道它是传递给回调的上下文,它不是我跟踪的东西,我知道我的对象实例
  •   

基本相同。您可以使用that取消引用绑定来解决此问题。我并不认为这是一个巨大的劣势,因为你直接使用对象“方法”作为回调的情况非常罕见 - 除了上下文之外,你经常想要提供额外的参数。顺便说一下,你在代码中也使用了that引用,它在那里被称为publicApi

  

为什么程序员不对非单例js类使用模块模式?

现在,你已经确定了一些不利之处。此外,您正在失去原型继承 - 具有其所有优点(简单性,动态性,instanceof,...)。当然,有些情况下它们不适用,而且您的工厂功能非常好。它确实用于这些情况。

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

您的代码的这些部分也有点令人困惑。你在这里用不同的对象覆盖变量,它发生在你的代码的中到端。最好将此视为“声明”(您在此处继承父属性!)并将其放在函数的顶部 - 正如var publicApi = $.extend(Animal(), {…});

此外,您不应在此处使用new关键字。您不要将这些函数用作构造函数,并且您不希望创建从Animal.prototype继承的实例(这会降低执行速度)。此外,它会使那些可能期望使用new的构造函数调用进行原型继承的人感到困惑。为清楚起见,您甚至可以将函数重命名为makeAnimalmakeSheep

  

在nodejs中对此有什么可比性?

此设计模式完全与环境无关。它将在Node.js中工作,就像在客户端和每个其他EcmaScript实现中一样。它的某些方面甚至与语言无关。

答案 2 :(得分: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

原型方法的另一个优点是节省内存。

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

这意味着每个新对象都会创建一个新的函数实例,因为它在原型方法的所有对象之间共享。 这种情况不会因为少数情况而有很大差异,但是当创建大量实例时,它会显示出相当大的差异。

此外,原型的另一个强大功能是,一旦创建了所有对象,您仍然可以一次更改所有对象之间的属性(只有在创建对象后它们不会被更改)。

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

<强>结论: 如果原型方法的上述优点对您的应用程序不那么有用,那么您的方法会更好。