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 只羊对 1000 只羊)。原型继承不应该被忽视,我们可以使用功能继承和原型继承的混合创建一个有效的模块模式。

正如文章JS - 为什么使用原型?,原型的优点之一是您只需要初始化原型成员一次,而构造函数中的成员是为每个实例创建的。实际上,您可以直接访问原型,而无需创建新对象。

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 和sheep。每个实例都会创建一些成员,例如 Animal.name,但我们知道 Animal.name 是静态的,因此最好实例化一次。由于您的代码暗示 Animal.name 应该在所有动物中都相同,因此如果我们将 Animal.prototype.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 中的事件委托

假设你有一个大杂货清单。百胜。

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

假设您要记录您单击的项目。一种实现是将事件处理程序附加到每个 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);
    }
}

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

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

经典的构造函数模式也是如此。顺便说一句,在您当前的代码中,是否animalHello并且getHelloCount应该是私有的还不是很清楚。如果您关心的话,在导出的对象文字中正确定义它们可能会更好。

  • 无需在回调中通过 $.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. 为清楚起见,您甚至可以将函数重命名为makeAnimaland 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

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

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

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

于 2013-05-25T07:27:52.043 回答