通过原型定义方法与在构造函数中使用原型定义方法——这确实是一个性能差异



在JavaScript中,我们有两种方法来创建"类"并赋予它公共函数。

方法1:

function MyClass() {
var privateInstanceVariable = 'foo';
this.myFunc = function() { alert(privateInstanceVariable ); }
}

方法2:

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

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

尽管在理论上,使用方法1为对象的每个实例提供了自己的函数副本(因此使用了更多的内存,更不用说分配所需的时间了),但在实践中实际会发生这种情况吗?web浏览器似乎可以很容易地进行优化,即识别这种极其常见的模式,并使对象引用的所有实例都是通过这些"构造函数"定义的函数的相同副本。然后,如果稍后显式更改,它只能为实例提供自己的函数副本

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

请参阅http://jsperf.com/prototype-vs-this

通过原型声明你的方法会更快,但这是否相关还有待商榷。

如果你的应用程序中存在性能瓶颈,那么不太可能是这样,例如,除非你碰巧在某个任意动画的每一步都实例化了10000多个对象。

如果性能是一个严重的问题,并且您希望进行微观优化,那么我建议通过原型进行声明。否则,只需使用对您最有意义的模式。

我要补充的是,在JavaScript中,有一种惯例,即在旨在被视为私有的属性前面加下划线(例如_process())。大多数开发商都会理解并避免这些房产,除非他们愿意放弃社会契约,但在这种情况下,你最好不要迎合他们。我想说的是:你可能真的不需要true私有变量。。。

在新版Chrome中,this.method比prototype.method快20%,但创建新对象的速度仍然较慢。

如果您可以重用对象而不是总是创建一个新对象,那么这可能比创建新对象快50%-90%。再加上无垃圾收集的好处,这是巨大的:

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

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

我在jsperf上创建了一个测试用例来演示这一点:

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

您可能没有考虑过这一点,但将方法直接放在对象上实际上在一个方面更好:

  1. 方法调用的速度略快(jsperf),因为解析该方法不必咨询原型链

然而,速度差几乎可以忽略不计。除此之外,在原型上使用一种方法有两种更具影响力的方式:

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

正如James所说,如果实例化一个类的数千个实例,那么这种差异可能很重要。

也就是说,我当然可以想象一个JavaScript引擎,它可以识别出您附加到每个对象的函数在实例之间不会发生变化,因此在内存中只保留该函数的一个副本,所有实例方法都指向共享函数。事实上,Firefox似乎在做一些像这样的特殊优化,但Chrome并没有。


ASIDE:

您是对的,不可能从原型上的方法内部访问私有实例变量。所以我想你必须问自己的问题是,你是否重视能够使实例变量真正私有化,而不是利用继承和原型设计?我个人认为,让变量真正私有化并没有那么重要,只需要使用下划线前缀(例如,"this._myVar")来表示尽管变量是公共的,但它应该被视为私有的。也就是说,在ES6中,显然有一种方法可以同时拥有两个世界!

您可以使用这种方法,它将允许您使用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();

简而言之,使用方法2创建所有实例都将共享的属性/方法。这些将是"全球性的",任何变化都将反映在所有情况下。使用方法1创建实例特定的属性/方法。

我希望我有一个更好的参考,但现在来看一下。您可以看到我是如何在同一个项目中将这两种方法用于不同的目的的。

希望这能有所帮助。:)

这个答案应该被视为对其余答案的扩展,以填补缺失的点。个人经验和基准都包含在内。

根据我的经验,无论方法是否私有,我都会使用构造函数来虔诚地构造我的对象。主要原因是,当我开始的时候,这是对我来说最简单的即时方法,所以这不是一个特别的偏好。它可能很简单,因为我喜欢可见的封装,原型有点脱离实体。我的私有方法也将作为范围中的变量进行分配。虽然这是我的习惯,可以很好地保持自我控制,但这并不总是最好的习惯,我有时确实会碰壁。除了根据配置对象和代码布局进行高度动态自组装的古怪场景外,在我看来,这往往是一种较弱的方法,尤其是在性能令人担忧的情况下。知道内部是私有的是有用的,但你可以通过其他方式和正确的纪律来实现这一点。除非性能是一个严肃的考虑因素,否则使用任何最有效的方法来完成手头的任务。

  1. 使用原型继承和约定将项标记为私有确实使调试更容易,因为您可以从控制台或调试器轻松地遍历对象图。另一方面,这样的约定使混淆变得更加困难,并使其他人更容易将自己的脚本安装到您的网站上。这也是私有范围方法广受欢迎的原因之一。这不是真正的安全,反而增加了阻力。不幸的是,很多人仍然认为这是一种真正的安全JavaScript编程方式。由于调试器已经变得非常好了,代码混淆就取而代之了。如果你正在寻找客户端存在太多安全缺陷的地方,这是你可能想要注意的设计模式
  2. 约定可以让您轻松地拥有受保护的财产。这可能是福也可能是祸。它确实缓解了一些继承问题,因为它的限制较少。在考虑可能访问房产的其他地方时,你仍然有发生碰撞或增加认知负荷的风险。自组装对象可以让你做一些奇怪的事情,你可以绕过许多继承问题,但它们可能是非常规的。我的模块往往有一个丰富的内部结构,在那里,除非外部需要,否则直到其他地方需要(共享)或暴露功能时,事情才会被拉出。构造函数模式往往会导致创建自包含的复杂模块,而不仅仅是零碎的对象。如果你想要,那没关系。否则,如果你想要一个更传统的OOP结构和布局,那么我可能会建议通过约定来规范访问。在我的使用场景中,复杂的OOP通常是不合理的,模块会起作用
  3. 这里的所有测试都是最低限度的。在现实世界的使用中,模块可能会更加复杂,这使得命中率比这里的测试要高得多。一个私有变量有多个方法处理它是很常见的,每一个方法都会在初始化时增加更多的开销,而原型继承不会带来这些开销。在大多数情况下,这并不重要,因为只有少数这样的对象可以四处浮动,尽管累积起来可能会增加
  4. 有一种假设是,由于原型查找,原型方法的调用速度较慢。这不是一个不公平的假设,我自己也做了同样的假设,直到我测试了它。事实上,它很复杂,一些测试表明这方面很琐碎。在prototype.m = fthis.m = fthis.m = function...之间,后者的性能明显优于前两者,前两者的性能大致相同。如果仅原型查找是一个重要问题,那么最后两个函数的性能将大大优于第一个函数。相反,至少在加那利看来,其他一些奇怪的事情正在发生。功能可能会根据其成员进行优化。许多性能考虑因素都在起作用。参数访问和变量访问也存在差异
  5. 内存容量。这里没有好好讨论。你可以预先假设,原型继承通常会更有内存效率,根据我的测试,这是普遍的。当你在构造函数中构建你的对象时,你可以假设每个对象可能都有自己的每个函数的实例,而不是共享的,一个更大的属性映射用于它自己的个人属性,并且可能会有一些开销来保持构造函数范围的开放。在私有作用域上操作的函数对内存的要求极其苛刻。我发现在很多情况下,内存的比例差异将比CPU周期的比例差异更显著
  6. 内存图。你也可以堵塞引擎,使GC更加昂贵。探查器确实倾向于显示这些天花在GC上的时间。这不仅仅是分配和释放更多资源的问题。您还将创建一个更大的对象图来遍历,这样GC就会消耗更多的周期。如果你创建了一百万个对象,然后几乎不接触它们,根据引擎的不同,它可能会对环境性能产生比你预期的更大的影响。我已经证明,这至少会使gc在处理对象时运行更长时间。也就是说,使用的内存和GC所需的时间往往存在相关性。然而,在某些情况下,无论内存如何,时间都是相同的。这表明图形构成(间接层、项目计数等)具有更大的影响。这不是一件总是容易预测的事情
  7. 没有多少人广泛使用链式原型,包括我自己,我不得不承认。原型链在理论上可能很昂贵。有人会的,但我没有衡量成本。如果您完全在构造函数中构建对象,然后在每个构造函数自身调用父构造函数时具有继承链,那么理论上方法访问应该更快。另一方面,如果重要的话,你可以完成等效的任务(例如将原型从祖先链上压平),如果你真的需要,你不介意破坏hasOwnProperty,也许instanceof等。在任何一种情况下,当你遇到性能黑客时,事情都会变得复杂。你最终可能会做一些不该做的事情
  8. 许多人不会直接使用你提出的任何一种方法。相反,他们使用匿名对象制作自己的东西,允许以任何方式共享方法(例如mixin)。还有许多框架实现了自己的策略来组织模块和对象。这些都是基于惯例的定制方法。对于大多数人和你来说,你的第一个挑战应该是组织而不是表现。这通常很复杂,因为与具有更明确的OOP/命名空间/模块支持的语言或平台相比,Javascript提供了许多实现目标的方法。当谈到性能时,我会说首先要避免重大陷阱
  9. 有一种新的Symbol类型应该适用于私有变量和方法。有多种方法可以使用它,它提出了一系列与性能和访问相关的问题。在我的测试中,Symbols的性能与其他任何东西相比都不太好,但我从未彻底测试过它们

免责声明:

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

最新更新