通过原型定义方法和在构造函数中使用this有性能差异吗?

64
在JavaScript中,我们有两种方法来创建一个“类”并赋予它公共函数。
方法1:
function MyClass() {
    var privateInstanceVariable = 'foo';
    this.myFunc = function() { alert(privateInstanceVariable ); }
}

方法二:
function MyClass() { }

MyClass.prototype.myFunc = function() { 
    alert("I can't use private instance variables. :("); 
}

我已经多次阅读到人们saying使用方法2更有效,因为所有实例共享函数的同一副本,而不是每个实例都有自己的副本。然而,通过原型定义函数有一个巨大的缺点-它使得不可能拥有私有实例变量。

虽然理论上,使用方法1会给对象的每个实例提供其自己的函数副本(因此使用更多内存,更不用说分配所需的时间了)-但在实践中是否真的如此?似乎Web浏览器可以轻松地优化这种极其常见的模式,并实际上使对象的所有实例引用通过这些“构造函数”定义的相同函数的副本。然后,只有在明确更改实例后,它才能给实例一个自己的函数副本。

任何关于两者之间性能差异的洞察力 - 或者更好的是现实世界的经验 - 都将非常有帮助。


7个回答

65

请查看 http://jsperf.com/prototype-vs-this

通过原型声明方法速度更快,但这是否相关存在争议。

除非你需要在任意动画的每个步骤中实例化10,000个以上的对象,否则在应用程序中出现性能瓶颈的可能性不大。

如果性能是一个严重问题,并且您想进行微调优化,则建议使用原型进行声明。否则,只需使用对您最有意义的模式即可。

我想补充的是,在JavaScript中,有一个约定:为打算视为私有的属性添加下划线前缀(例如,_process())。除非开发人员愿意放弃社交协议,否则大多数开发人员将理解并避免使用这些属性,但在这种情况下,您可能不需要真正的私有变量...


4
@RajV,原型方法只需声明一次。内部函数(非原型)需要在每个实例化中声明--我认为这就是使该方法变慢的原因。调用该方法可能实际上会更快,就像你说的那样。 - James
1
@999 你说得对。我没有注意到测试在循环内创建了一个新实例。但是,有趣的是,我将测试用例更改为仅测试方法调用的开销。http://jsperf.com/prototype-vs-this/2。即使在那里,你会发现调用原型方法大约快10%。有任何想法为什么吗? - RajV
2
@RajV,你的测试在每次迭代中仍然运行'new T`。JSperf网站将自动测试您的片段数百万次。您不需要添加自己的循环。看这里:http://jsperf.com/prototype-vs-this/3 ... 尽管结果似乎相同。原型方法调用略快,这很奇怪。 - James
4
在2016年,这个观点仍然适用吗? - Eric
1
参考链接已不再可用。 - Anson
显示剩余6条评论

2
在新版本的Chrome中,this.method比prototype.method快约20%,但创建新对象仍然较慢。如果您可以重复使用对象而不是总是创建新对象,则这可能比创建新对象快50%至90%。另外,没有垃圾回收的好处也非常大:

http://jsperf.com/prototype-vs-this/59


3
看起来 jsperf.com 不再活跃了。你有其他性能测量数据吗? - Eric
jsPerf又恢复了。在Chrome 55中进行的这个测试给出了相同的结果,而在Firefox 50中使用“this”要快三倍。 - Yay295
那个测试是错误的。在第一个测试中,您会在每次迭代中实例化类,然后调用方法。而在第二个测试中,您只会实例化一次类,然后在每次迭代中仅调用该方法。 - jgmjgm

1
你可以使用这种方法,它将允许你使用prototype并访问实例变量。
var Person = (function () {
    function Person(age, name) {
        this.age = age;
        this.name = name;
    }

    Person.prototype.showDetails = function () {
        alert('Age: ' + this.age + ' Name: ' + this.name);
    };

    return Person; // This is not referencing `var Person` but the Person function

}()); // See Note1 below

注意1:
括号将调用函数(自执行函数)并将结果分配给var Person

使用方法

var p1 = new Person(40, 'George');
var p2 = new Person(55, 'Jerry');
p1.showDetails();
p2.showDetails();

但是每个实例仍然会创建一个新的方法,因此在这里使用原型并没有节省内存。 - Richard Scarrott
@riscarrott 不,它不会在每个实例中都创建。只有构造函数会在每个实例中调用。你也可以轻松地通过这种方式进行检查:p1.showDetails === p2.showDetails 来证明它是同一个函数。 - CodingYoshi
啊,抱歉,我看错了。那么,将其包装在自调用函数中有什么好处呢? - Richard Scarrott
你立即执行它,所以“Person”在此后被定义并可供使用。使用这种方法,您也可以定义“静态”方法。基本上由于JavaScript没有类,这种方法试图适应这种限制。您可以在这里阅读更多相关信息。 - CodingYoshi

1

只有在创建大量实例时才会有所区别。否则,在这两种情况下调用成员函数的性能完全相同。

我已经在jsperf上创建了一个测试用例来证明这一点:

http://jsperf.com/prototype-vs-this/10


1
您可能没有考虑到,但是在对象上直接放置方法实际上有一个优点:
  1. 方法调用会更快速一些jsperf),因为不必查找原型链以解决该方法。

然而,速度差异几乎可以忽略不计。最重要的是,将方法放置在原型中有两个更具影响力的优点:

  1. 更快地创建实例jsperf
  2. 使用更少的内存

正如James所说,如果您要实例化数千个类的实例,则此差异可能很重要。

话虽如此,我完全可以想象一种JavaScript引擎,它会识别您附加到每个对象的函数并且不会在实例之间更改,因此只保留内存中的一个函数副本,并使所有实例方法指向共享函数。实际上,似乎Firefox正在执行此类特殊优化,但Chrome不是。


附言:

你说得对,无法从原型内部的方法访问私有实例变量。因此,我想你必须问自己,你更重视能够使实例变量真正私有还是利用继承和原型?我个人认为,使变量真正私有并不那么重要,只需使用下划线前缀(例如,“this._myVar”)表示尽管变量是公共的,但应被视为私有。话虽如此,在ES6中,显然有一种方式可以拥有两全其美的效果!


你的第一个jsperf测试用例有缺陷,因为你只是一遍又一遍地在同一个实例上调用该方法。事实上,引擎(包括FF和Chrome)确实会对此进行大量优化(就像你想象的那样),这里发生的内联使得你的微基准测试完全不现实。 - Bergi
@Bergi JSPerf说它会在计时测试循环之前运行设置代码,而不是在定时代码区域内。我的设置代码使用 new 创建了一个新实例,这不意味着该方法确实不会一遍又一遍地调用同一个对象吗? 如果JSPerf不能在每个测试循环中进行"沙盒化",那么我认为它并没有太大的用处。 - Niko Bellic
1
不,这是一个“测试循环” - 你的代码在循环中运行以测量速度。这个测试会被执行多次以获得平均值,在每个测试及其相应的循环之前都会运行设置。 - Bergi
1
你可以在设置阶段预先创建多个实例,然后在定时部分使用 var x = instances[Math.floor(Math.random()*instances.length)]; x.myMethod()。只要所有测试中的 var x = … 行相同(并且执行相同),任何速度差异都可以归因于方法调用。如果您认为 Math 代码太重,也可以尝试在设置中创建一个大的 instances 数组,然后在测试中对其进行循环 - 只需确保循环不会展开即可。 - Bergi
是的,它们看起来很不错 :-) 有趣的是,我本以为原型版本会更好 - 它总是查找相同的函数,并且可以学习内联它。也许使用 Object.create 而不是 new,或者创建 10000 个不同的原型对象(而不仅仅是 10 个)会影响这个 - 引擎优化是靠不住的。 - Bergi
显示剩余3条评论

0

这个答案应该被视为填补其他答案中缺失的要点的扩展。个人经验和基准测试都被纳入考虑。

就我的经验而言,无论方法是私有的还是公有的,我都会虔诚地使用构造函数来构建我的对象。主要原因是当我开始时,这是对我来说最容易立即实现的方法,所以它并不是一种特殊的偏好。可能仅仅是因为我喜欢可见的封装,而原型有点脱离实体。我的私有方法也将在作用域中分配为变量。虽然这是我的习惯,并且使事情保持得很好,但并不总是最好的习惯,有时我也会遇到困难。除了高度动态自组装的奇怪场景和代码布局之外,如果性能是一个问题,它往往是较弱的方法。知道内部是私有的很有用,但是您可以通过正确的纪律采取其他手段来实现这一点。除非性能是一个严重的考虑因素,否则根据手头任务使用最好的方法。

使用原型继承和标记项目为私有的约定确实使调试更容易,因为您可以轻松地从控制台或调试器遍历对象图。另一方面,这种约定使混淆变得更加困难,并使其他人更容易将自己的脚本添加到您的站点上。这是私有范围方法变得流行的原因之一。它不是真正的安全性,而是增加了抵抗力。不幸的是,很多人仍然认为这是一种真正编写安全JavaScript的方式。由于调试器已经变得非常好,代码混淆取代了它的位置。如果您正在寻找在客户端过多的安全漏洞,则可能需要注意设计模式。
约定允许您轻松拥有受保护的属性。这可能是一种祝福,也可能是一种诅咒。它确实减轻了一些继承问题,因为它不那么限制。您仍然有冲突的风险或者考虑其他地方可能访问属性时的认知负荷增加。自组装对象可以让您做一些奇怪的事情,您可以解决许多继承问题,但它们可能是非传统的。我的模块倾向于具有丰富的内部结构,直到功能在其他地方(共享)需要时才会将其拆分出来,或者除非需要在外部使用。构造函数模式往往会导致创建自包含的复杂模块,而不是简单的零散对象。如果您想要这样做,那么没问题。否则,如果您想要更传统的OOP结构和布局,那么我可能建议按约定规范访问。在我的使用场景中,复杂的OOP并不经常被证明是合理的,而模块可以胜任。
这里的所有测试都是最小的。在实际使用中,模块很可能会更加复杂,使得击中比这里的测试所示的要大得多。通常情况下,私有变量具有多个方法对其进行操作,每个方法都会增加更多的开销,这是您在原型继承中不会遇到的。在大多数情况下,这并不重要,因为只有少数这样的对象实例浮动,尽管累积起来可能会增加。
有一个假设,即原型方法调用速度较慢,因为需要查找原型。这不是不公平的假设,我自己也做了同样的事情,直到我测试过它。实际上,这很复杂,一些测试表明这个方面是微不足道的。在prototype.m = fthis.m = fthis.m = function...之间,后者比前两者表现得更好,前两者的表现大致相同。如果仅仅原型查找就是一个重要问题,那么后两个函数将显著优于第一个函数。相反,至少在Canary中发生了一些奇怪的事情。可能根据成员进行了函数优化。许多性能考虑因素都会产生影响。您还需要考虑参数访问和变量访问的差异。
内存容量。这里没有很好地讨论。您可以预先做出的假设是,原型继承通常会更加节省内存,并

免责声明:

  1. 关于性能有很多讨论,但并不总是有一个永久正确的答案,因为使用场景和引擎会发生变化。始终进行分析,但也要以多种方式进行测量,因为分析并不总是准确可靠的。除非确实存在可证明的问题,否则避免将大量精力投入到优化中。
  2. 最好的方法可能是在自动化测试中包含敏感区域的性能检查,并在浏览器更新时运行。
  3. 记住有时电池寿命也很重要,而不仅仅是可感知的性能。最慢的解决方案可能在运行优化编译器后变得更快(例如,编译器可能比按约定标记为私有的属性更好地了解何时访问受限范围变量)。考虑后端,如node.js。这可能需要比您通常在浏览器上找到的更好的延迟和吞吐量。大多数人不需要担心这些事情,比如注册表单的验证,但此类事情可能会影响越来越多的不同场景。
  4. 在持久化结果之前,必须小心使用内存分配跟踪工具。在某些情况下,如果我没有返回和持久化数据,它将被完全优化掉,或者实例化/未引用之间的采样率不足,让我想知道如何将初始化并填充到一百万个数组注册为3.4KiB在分配配置文件中。
  5. 在现实世界中,在大多数情况下,真正优化应用程序的唯一方法是首先编写它,以便您可以对其进行测量。在任何给定的情况下,可能会涉及几十到数百个因素,甚至数千个因素。引擎还会执行可能导致非对称或非线性性能特征的操作。如果在构造函数中定义函数,则它们可能是箭头函数或传统函数,每个函数在某些情况下的行为都不同,而我对其他函数类型一无所知。类也不会在原型构造函数的性能方面表现相同,即使它们应该是等效的。您需要非常小心地处理基准测试。原型类可以以各种方式延迟初始化,特别是如果您也将属性原型化(建议不要这样做)。这意味着您可能会低估初始化成本并高估访问/属性突变成本。我还看到过渐进式优化的迹象。在这些情况下,我用相同的对象实例填充了一个大数组,随着实例数量的增加,对象似乎逐步优化为内存,直到剩余部分相同。这些优化也可能显著影响CPU性能。这些事情非常依赖于您编写的代码以及运行时发生的事情,例如对象数量,对象之间的差异等。

0
简而言之,使用方法2创建所有实例都将共享的属性/方法。这些将是“全局”的,对其进行的任何更改都将反映在所有实例中。使用方法1创建特定于实例的属性/方法。
我希望我有一个更好的参考,但现在先看看this。您可以看到我在同一项目中为不同目的使用了两种方法。
希望这有所帮助。 :)

你的链接已经失效了。你可以在回答中添加代码来说明你的观点吗? - Paul

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接