【问题标题】:Functions inside constructor vs prototype构造函数与原型中的函数
【发布时间】:2015-05-27 15:59:27
【问题描述】:

我知道有类似的问题,但我想看看这些答案是否仍然有效,因为在新的 Javascript 引擎中进行了优化。

在我看来,在构造函数中定义函数的最大好处是您可以轻松避免必须知道 'this' 关键字的值:

var Person = function() {
  var self = this;
  self.firstName = null;
  self.lastName = null;
  self.fullName = function() {
    return self.firstName + self.lastName;
  };
};

Knockout Managing 'this' 推荐这种方法。这是一个很大的优势,尤其是当许多开发人员正在修改代码时,因为它非常易于理解和使用。

另一种方法是使用对象原型:

var Person = function() {
  this.firstName = null;
  this.lastName = null;
};
Person.prototype.fullName = function() {
  return this.firstName + this.lastName;
};

在这种情况下有性能优势,因为函数对象将被创建一次。然而,我遇到的主要问题是处理“this”关键字可能很复杂。上面的例子很简单,但是如果你有事件处理程序、forEach 调用、jQuery each() 调用、从不同上下文调用的方法等等,很容易对 this 造成不好的利用。

当然,如果您了解“this”的工作原理并且知道方法是如何被调用的,那么您应该不会有太多问题。但是,根据我的经验,这需要时间并且容易出错,尤其是当代码由许多开发人员编写时。

我知道新的 JS 引擎,比如 V8,正在对通过创建隐藏类在构造函数中声明函数的情况进行优化:How the V8 engine works?

所以我的问题是,考虑到新 JS 引擎完成的这些优化以及必须处理“this”关键字的复杂性,使用基于原型的方法是否仍然有意义?使用将所有内容都放入构造函数的方法我会丢失什么?

更新 1:

我刚刚在 Chrome(42 版)上做了一个微基准测试。我创建了 1M 个对象,其中包含构造函数中的函数和原型中的函数。这是一个非常简单的对象,有两个变量和三个函数,结果如下:

Functions inside constructor: 1.91 seconds
Functions in prototype: 1.10 seconds

听起来即使在 V8 中进行了这些优化,它仍然快 73%。然而,这是一个微基准。不确定这是否会对现实世界的应用程序产生重大影响。

更新 2:

我还查看了内存消耗,也存在很大差异。对于构造函数内部的函数:

Shallow size: 64,000,120
Retained size: 336,001,128

对于原型函数:

Shallow size: 40,000,000
Retained size: 40,000,000

隐藏类的优化不是那么好,或者我错过了一些东西。我正在使用 V8 建议的单态代码(没有 args 的构造函数),但不确定我是否做错了什么。

更新 3:

这是我所做的测试的链接,以防有人指出其中的错误:http://jsperf.com/dg-constructor-vs-prototype

【问题讨论】:

  • FWIW,我添加了第三个测试用例,“POJO”不使用原型或“new”关键字,它只是创建一个具有公共方法和私有变量的对象。它在性能上与构造函数示例相当。无论哪种方式,原型方法总是更快,尽管增加了复杂性。 jsperf.com/dg-constructor-vs-prototype/2
  • 谢谢斯科特。是的,听起来目前最有效的方法是使用原型。很遗憾,因为我喜欢将 'this' 分配给 'self' 的简单性,并确保 'self' 指向对象,而不必考虑 'this' 指向什么。
  • 是的,我完全同意。对于简单的对象(没有继承),使用prototype无非是一种微优化,缺点是必须仔细理解和管理this的使用。

标签: javascript


【解决方案1】:

我进行了快速测试。如果在构造函数中声明函数,即使经过优化,两个对象实例也会有不同的函数实例。但是对于原型,您只有一个解释性能差异的函数实例。

    var Person = function () {
        var self = this;
        self.firstName = null;
        self.lastName = null;
        self.fullName = function () {
            return self.firstName + self.lastName;
        };
    };

    Person.prototype.fullName2 = function () {
        return this.firstName + this.lastName;
    };

    var a = new Person();
    var b = new Person();

    console.log(a.fullName == b.fullName); // returns false
    console.log(a.fullName2 == b.fullName2); // returns true

【讨论】:

  • 是的,我用我做的一些测试更新了这个问题。使用优化时不应该创建一个函数吗?我想知道是否需要以某种特殊方式创建对象才能利用该优化。
  • 好像没有办法优化。即使 v8 创建了隐藏类,构造函数中的函数也是实例变量。我能想到的唯一解决方法是使用原型,并像self 一样定义var pro = Person.prototype;。然后用pro.fullName = function() 代替self.fullName = function()。至少代码看起来很相似 :) 对于this 问题,可以应用类似think-robot.com/2009/06/… for jQuery 的东西。
  • 使用'self'的好处是你可以在任何知道它指向对象的地方使用。我正在使用一些类似于您所说的解决方法,但您需要记住这样做。但是,是的,为了提高效率,到目前为止可能没有其他选择。
【解决方案2】:

就像@Ersin Basaran 提到的,在构造函数中创建的函数对于每个对象实例都是唯一的,与使用原型创建的函数不同,它使每个对象实例的函数都相同。

但是,在 ES6(ECMAScript2015)中引入类之后,如果你使用类来创建方法而不是使用构造函数,并且你在构造函数之外(但在类内部)创建了这个方法,就会一样对于每个对象实例,就像使用原型时一样。

下面是创建fullName() 方法的示例:

class Person {
    constructor () {
        var self = this;
        self.firstName = null;
        self.lastName = null;
    }
    fullName () {
        return self.firstName + self.lastName;
    }
}

Person.prototype.fullName2 = function () {
    return this.firstName + this.lastName;
};

var a = new Person();
var b = new Person();

console.log(a.fullName == b.fullName); // returns true
console.log(a.fullName2 == b.fullName2); // returns true

我希望这会有所帮助。

【讨论】:

    【解决方案3】:

    我一直在使用一种稍微不同的方法,IMO 具有清晰的优势,并且还避免了每次调用构造函数时都重新创建成员函数:

    • 将类的成员函数定义为构造函数之外的模块级函数
    • 在构造函数中将函数显式分配给类实例

    例如:

    // Constructor for MyClass
    function MyClass(a, b){
    
        // set properties of the instance from constructor arguments
        this.a = a;
        this.b = b;
    
        // assign the report function as a member of this instance
        this.report = Report;
    
    }
    
    // Report function is defined at the module level,
    // but used by assigning it to an instance variable
    // within the constructor.
    function Report(){
        console.log( "a=" + this.a, "b=" + this.b);
    }
    

    只有一个成员函数的实例被同一个类的所有实例共享 - 就像将函数分配给 class.prototype.function 时的情况一样 - 所以这种方法是有效的,并且具有这些额外的优点,海事组织:

    1. 构造函数显式包含类的所有方法的声明,而不是在构造函数之外将方法添加到原型中。
    2. 与在构造函数中编写整个函数定义相比,构造函数的代码更简洁,更易于理解。
    3. 由于方法是在模块级别定义的,它在模块执行之前被实例化,因此可以在构造函数声明之前的代码中引用该类。分配给 class.prototype.function 时不是这种情况,必须在调用构造函数之前执行。

    用法:

    // Create instances of my class
    var inst1 = new MyClass( "as", "df");
    var inst2 = new MyClass( "gh", "jk");
    
    // Report the instances
    inst1.report();
    inst2.report();
    
    // Class and method declarations follow below here...
    

    这种方法有什么缺点吗?

    【讨论】:

    • 这看起来与原型方法非常相似。另一种方法的优点之一是您可以将this 分配给另一个变量,例如self,这是原型方法无法做到的,并且使事情变得更简单。我认为你不能用这种方法做到这一点。
    • Hmm.. 我想你也可以在我的方法中使用var self = this,但实际上它不是必需的,因为函数内部的this 并不模棱两可——它总是指对象的实例通过它在调用时调用函数。如果要确保它引用当时正在构造的特定实例,则仅在构造函数中声明函数时才需要它。