【问题标题】:Simple javascript inheritance using $.extend and module pattern使用 $.extend 和模块模式的简单 javascript 继承
【发布时间】:2013-05-15 14:09:30
【问题描述】:

几年来,我一直想知道人们如何看待使用模块模式式构造函数模式进行继承而没有正常的原型继承。为什么程序员不对非单例 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();

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

谢谢!

[更新]

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

【问题讨论】:

    标签: javascript jquery inheritance module


    【解决方案1】:

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

    正如帖子JS - Why use Prototype?中提到的, 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 !
    }
    

    对比原型

    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”指针
    • 没有更多的 var that = 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 应用程序并且这些性能问题不是问题, 那么您的方法将是可行的方法。

    【讨论】:

    • 您的 Animal.prototype.updateName('NewName'); 不会修改所有动物 - 它只会修改所有动物继承的 Generic 名称。顺便说一句,您的组合脚本包含一些不好的做法,并非在所有情况下都有效,并且仍然对Sheep 使用功能/寄生继承。它没有显示出 OP 的任何优势……
    • 是的,您对 Animal.prototype.updateName('NewName') 仅更新 Generic 名称是正确的,但这是预期的效果。所有的羊都有一个通用名称(因为它们仍然是动物)和它们自己的名字。
    • 随时更新我的​​代码。我的观点是,当前使用 $.extend 会重新初始化 Animal.NameAnimal.IsAnimal 等常见属性,并且稍后会覆盖 Name 等一些属性。为了充分利用 $.extend 我认为应该使用代码,以便扩展只发生一次以构建基本 Sheep 对象,然后应该使用构造函数/函数来初始化值(如 makeSheep 函数你描述)而不是每次都做多余的扩展。
    【解决方案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 进行原型继承的人混淆。为清楚起见,您甚至可以将函数重命名为 makeAnimalmakeSheep

    在 nodejs 中与此类似的方法是什么?

    这种设计模式完全独立于环境。它可以在 Node.js 中工作,就像在客户端和所有其他 EcmaScript 实现中一样。它的某些方面甚至与语言无关。

    【讨论】:

    • 您的意思是instanceof 哪里有typeof?不管有没有原型继承,typeof 只会给你"object""function",永远不会给你"Animal"Animal
    【解决方案3】:

    使用您的方法,您将无法如此方便地覆盖函数并调用超级函数。

    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
    

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

    【讨论】:

    • 当然可以覆盖方法:function Bar() { var that = Foo(), superValue = that.value; that.value = function() { return superValue()+" overwritten"; }; return that; }
    猜你喜欢
    • 2014-08-11
    • 1970-01-01
    • 2023-03-21
    • 1970-01-01
    • 2013-04-28
    • 1970-01-01
    • 1970-01-01
    • 2013-10-31
    • 2016-06-24
    相关资源
    最近更新 更多