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

IT技术 javascript performance memory-management prototype
2021-01-15 14:32:51

在 JavaScript 中,我们有两种方法可以创建“类”并为其提供公共函数。

方法一:

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

方法二:

function MyClass() { }

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

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

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

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

6个回答

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

通过原型声明你的方法更快,但这是否相关是有争议的。

例如,如果您的应用程序存在性能瓶颈,则不太可能出现这种情况,除非您碰巧在某个任意动画的每一步都实例化了 10000 多个对象。

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

我要补充一点,在 JavaScript 中,有一个约定,为属性添加前缀,这些属性旨在被视为带有下划线的私有属性(例如_process())。大多数开发人员会理解并避免这些属性,除非他们愿意放弃社会契约,但在这种情况下,您还不如不去迎合他们。我的意思是说:你可能真的不需要真正的私有变量......

参考链接不再可用。
2021-03-12 14:32:51
@RajV,原型方法只声明一次。内部函数(非原型)需要在每次实例化时声明——我认为这就是使这种方法变慢的原因。正如您所说,该方法的调用实际上可能更快。
2021-03-13 14:32:51
@RajV,您的测试仍在每次迭代中运行“新 T”。JSperf 站点将自动测试您的代码片段数百万次。您不需要添加自己的循环。请参阅此处:jsperf.com/prototype-vs-this/3 ...不过结果似乎相同。原型方法调用稍微快一点,这很奇怪。
2021-03-19 14:32:51
@999 你是对的。我没有注意到测试正在循环中创建一个新实例。但是,有趣的是。我将测试用例更改为只测试方法调用的费用。jsperf.com/prototype-vs-this/2即使在那里,您也会看到调用原型方法的速度提高了大约 10%。知道为什么吗?
2021-03-28 14:32:51
这在 2016 年仍然适用吗?
2021-04-10 14:32:51

在新版本的Chrome中,this.method比prototype.method快20%左右,但是创建新对象的速度还是比较慢。

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

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

那个测试是错误的。在第一个中,您实例化类,然后每次迭代调用该方法。在第二个中,您将类实例化一次,然后每次迭代只调用该方法。
2021-03-19 14:32:51
看起来 jsperf.com 的活动时间更长了。你还有其他性能测量吗?
2021-03-21 14:32:51
jsPerf 再次启动。Chrome 55 中的这个测试给出了相同的结果,而this在 Firefox 50 中使用速度快了三倍。
2021-03-27 14:32:51

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

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

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

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

  1. 方法调用速度非常jsperf),因为不必参考原型链来解析方法。

但是,速度差异几乎可以忽略不计。最重要的是,将方法放在原型上有两种更有影响力的方式更好:

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

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

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


在旁边:

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

您的第一个 jsperf 测试用例有缺陷,因为您只是一次又一次地在同一个实例上调用该方法。事实上,引擎(FF 和 Chrome)确实对此进行了大量优化(就像您想象的那样),并且这里发生的内联使您的微基准测试完全不切实际。
2021-03-11 14:32:51
@Bergi JSPerf 说它在“每个计时测试循环之前,在计时代码区域之外”运行设置代码。我的设置代码使用创建了一个新实例new,所以这是否意味着该方法确实不会一次又一次地在同一个对象上调用?如果 JSPerf 没有“沙箱”每个测试循环,我认为它不会很有用。
2021-03-11 14:32:51
啊我明白了。感谢您的澄清。我摆弄了 JSPerf 并同意你的观点。为了保证每次在实例上调用 myMethod 时都使用不同的实例,我需要在测试代码中创建一个新实例,而不是设置代码。这样做的问题是,测试还将包括实例化实例所需的时间,当我真的只想测量在实例上调用方法所需的时间......任何方式来处理这个JSPerf?
2021-03-20 14:32:51
您可以事先(在设置中)创建多个实例,然后var x = instances[Math.floor(Math.random()*instances.length)]; x.myMethod()在定时部分使用。只要var x = …所有测试中行都相同(并且执行相同的操作),速度的任何差异都可以归因于方法调用。如果您认为Math代码太重,您还可以尝试instances在 setup 中创建一个大数组,然后在测试中放置一个循环 - 您只需要确保循环不会展开。
2021-04-01 14:32:51
不,这是一个“测试循环”——您的代码在循环中运行以测量速度。此测试执行多次以获得平均值,并且在每个测试及其各自的循环之前运行设置。
2021-04-06 14:32:51

简而言之,使用方法 2 创建所有实例将共享的属性/方法。这些将是“全局的”,对它的任何更改都将反映在所有实例中。使用方法 1 创建实例特定的属性/方法。

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

希望这可以帮助。:)

您的链接不再有效。你能在你的答案中添加代码来说明你的观点吗?
2021-03-31 14:32:51