通过原型定义方法vs在构造函数中使用它 – 真的是性能差异?
在JavaScript中,我们有两种方法来创build一个“类”并赋予其公共职能。
方法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更有效,因为所有的实例共享相同的函数副本,而不是每个都获得自己的副本。 通过原型定义函数有一个巨大的缺点 – 它使得不可能有私有的实例variables。
尽pipe从理论上讲,使用方法1给每个对象的实例提供了自己的函数副本(因此使用更多的内存,更不用说分配所需的时间了) – 实际上实际发生了什么? 看起来像一个优化的Web浏览器可以很容易地认识到这个非常普遍的模式,并且实际上所有的对象实例引用通过这些“构造函数”定义的相同的函数副本。 然后,如果稍后显式更改,则只能为该实例提供自己的副本。
任何见解 – 或者甚至更好的现实世界的经验 – 关于两者之间的performance差异将是非常有用的。
见http://jsperf.com/prototype-vs-this
通过原型声明你的方法更快,但不pipe这是否相关是值得商榷的。
例如,如果您的应用程序中存在性能瓶颈,则不太可能是这样,除非您恰好在某些任意animation的每一步中实例化了10000多个对象。
如果性能是一个严重的问题,你想微观优化,那么我会build议通过原型声明。 否则,只要使用对你最有意义的模式。
我将补充一点,在JavaScript中,有一个前缀属性的约定,意图被视为私有的下划线(例如_process()
)。 大多数开发商会理解和避免这些属性,除非他们愿意放弃社会契约,但在这种情况下,你可能不会迎合他们。 我的意思是说:你可能不需要真正的私有variables
在新版本的Chrome中,this.method比prototype.method快大约20%,但创build新对象的速度仍然较慢。
如果您可以重复使用该对象,而不是始终创build一个新对象,则可以比创build新对象快50%至90%。 加上没有垃圾收集的好处,这是巨大的:
只有在创build大量实例时才有所作为。 否则,调用成员函数的性能在两种情况下都是完全相同的。
我在jsperf上创build了一个testing用例来演示这个:
你可能没有考虑到这一点,但直接将方法直接放在对象上的方式实际上更好:
- 方法调用的速度要稍微快一些 ( jsperf ),因为原型链不需要咨询来解决方法。
但是,速度差异几乎可以忽略不计。 最重要的是,在原型上添加一个方法更有两个更有效的方法:
- 更快创build实例 ( jsperf )
- 使用较less的内存
就像詹姆斯说的那样,如果你正在实例化一个类的数千个实例,那么这种差异就很重要。
也就是说,我当然可以想象一个JavaScript引擎,它可以识别你连接到每个对象的函数不会在实例间发生变化,因此只会保留内存中的一个副本,所有实例方法都指向共享函数。 事实上,Firefox似乎正在做一些特殊的优化,但Chrome并不是。
在旁边:
你是对的,不可能从原型的内部方法访问私有的实例variables。 所以我想你必须问自己的问题是,你是否重视能够使用inheritance和原型来使实例variables真正的私有? 我个人认为把variables变成真正的私有并不重要,只要使用下划线前缀(例如“this._myVar”)来表示尽pipevariables是公开的,但它应该被认为是私有的。 也就是说,在ES6中,显然有一个两全其美的方法!
简而言之,使用方法2创build所有实例将共享的属性/方法。 这些将是“全球化”的,任何改变都将反映在所有情况下。 使用方法1创build实例特定的属性/方法。
我希望我有一个更好的参考,但现在看看这个 。 你可以看到我如何在同一个项目中使用两种方法来达到不同的目的。
希望这可以帮助。 🙂
这个答案应该被认为是填补了缺点的其余答案的扩展。 个人经验和基准都被纳入。
就我的经验而言,我使用构造函数来虔诚地构build我的对象,无论方法是否私有。 主要的原因是,当我开始时,这是对我来说最简单的直接方法,所以这不是一个特殊的偏好。 这可能是一样简单,我喜欢可见的封装和原型有点无形。 我的私有方法也将作为范围内的variables。 尽pipe这是我的习惯,并且保持了很好的自我包容,但并不总是最好的习惯,而且我有时也会碰壁。 除了根据configuration对象和代码布局进行高度dynamic自组装的怪异场景,在我看来,这往往是更弱的方法,特别是如果性能是一个问题。 知道内部是私人的是有用的,但你可以通过其他方式来实现这一点,正确的纪律。 除非性能是一个严肃的考虑,否则使用任何最好的工具,否则为手头的任务。
- 使用原型inheritance和约定将项目标记为私有会使得debugging变得更简单,因为您可以轻松地从控制台或debugging器遍历对象graphics。 另一方面,这样的惯例会使混淆变得更加困难,并且使其他人更容易将自己的脚本粘贴到您的网站上。 这是私人范围方法获得普及的原因之一。 这不是真正的安全,而是增加了阻力。 不幸的是,很多人仍然认为这是编写安全JavaScript的真正方法。 由于debugging器变得非常好,代码混淆取代了它的地位。 如果您正在寻找客户端上安全漏洞太多的地方,那么这是您可能需要注意的一种devise模式。
- 一个约定允许你有一点小题大做的保护属性。 这可能是一个祝福和诅咒。 它确实缓解了一些inheritance问题,因为它的限制性较小。 在考虑其他地方可能会被访问的情况下,您仍然有碰撞或增加认知负担的风险。 自我组装的东西让你做一些奇怪的事情,你可以解决一些inheritance问题,但它们可以是非常规的。 我的模块往往有一个丰富的内部结构,其中的东西不被拉出来,直到function需要在别处(共享)或暴露,除非需要外部。 构造函数模式往往会导致创build自包含的复杂模块,而不仅仅是零碎的对象。 如果你想,那就没事了。 否则,如果你想要一个更传统的面向对象的结构和布局,那么我可能会build议按照惯例调整访问。 在我的使用场景中,复杂的OOP并不经常被certificate是正确的,模块可以做到这一点。
- 这里的所有testing都是最小的。 在现实世界的使用情况下,模块可能会更复杂,使得命中率远远超过这里所指出的testing。 有一个私有variables与多个方法的工作是相当普遍的,每个这些方法将增加更多的初始化开销,你不会得到原型inheritance。 在大多数情况下是没有关系的,因为只有less数这样的对象的实例浮起来,尽pipe累积起来可能会累加起来。
- 有一个假设,原型方法由于原型查找而调用较慢。 这不是一个不公平的假设,我自己也一样,直到我testing它。 实际上这很复杂,有些testing表明这个方面是微不足道的。 在之间,
prototype.m = f
,this.m = f
和this.m = function...
后者performance明显好于前两个执行大约相同。 如果单纯的原型查找是一个重大的问题,那么最后两个函数会显着地执行第一个function。 相反,其他奇怪的事情至less在金丝雀所关心的地方正在发生。 可能的function是根据它们的成员进行优化的。 多种性能考虑因素发挥作用。 参数访问和variables访问也有差异。 - 内存容量。 这里没有很好的讨论。 你可以做出一个假设,这很可能是真实的,原型inheritance通常会更有效率的记忆,根据我的testing,通常是这样。 当你在你的构造函数中构build你的对象时,你可以假设每个对象可能都有自己的每个函数的实例,而不是共享的,一个更大的属性映射用于它自己的个人属性,并且可能还有一些开销来保持构造函数的作用域。 在私人范围内运作的function对存储器的要求极高且不成比例。 我发现在很多情况下,内存中的比例差异比CPU周期中的比例差异要大得多。
- 内存图。 您也可以阻塞引擎,使GC更昂贵。 分析器倾向于显示这些天在GC中花费的时间。 分配和解放问题不仅是一个问题。 你也会创build一个更大的对象图来遍历,所以GC消耗更多的周期。 如果您创build了一百万个对象,然后几乎不触及它们,则取决于引擎,可能会产生比预期更多的环境性能影响。 我已经certificate,这至less可以使物体在处理物体时运行更长的时间。 这往往与使用的内存和GC所花的时间有关。 但是,无论内存如何,时间都是一样的。 这表明graphics化妆(间接,项目数量等)有更多的影响。 这并不总是容易预测的。
- 广泛使用链式原型的人不多,我自己也不得不承认。 原型链在理论上可能是昂贵的。 有人会但我没有测量成本。 如果你在构造函数中完全build立你的对象,然后有一个inheritance链,因为每个构造函数调用一个父构造函数,理论上来说方法访问应该快得多。 另一方面,如果重要的话,你可以完成相同的事情(比如将原型平铺在祖先链上),如果你真的需要的话,你不介意像hasOwnProperty,也许是instanceof等。 在这两种情况下,事情开始变得复杂一旦你在这条道路上performance黑客。 你可能最终会做你不应该做的事情。
- 许多人不直接使用你提出的方法。 相反,他们使用匿名对象做自己的事情,允许方法共享任何方式(例如mixin)。 还有一些框架可以实现自己的组织模块和对象的策略。 这些都是基于惯例的习惯方法。 对于大多数人和你来说,你的第一个挑战应该是组织而不是performance。 这通常是很复杂的,因为Javascript提供了许多方法来实现语言或者具有更明确的OOP /名称空间/模块支持的平台。 谈到表演,我会说,而不是首要的主要陷阱。
- 有一个新的符号types应该为私有variables和方法工作。 有很多方法可以使用它,并引发了一系列与性能和访问相关的问题。 在我的testing中,符号的performance与其他一切相比都不是很好,但我从来没有彻底地testing过。
免责声明:
- 关于性能的讨论很多,在使用场景和引擎发生变化时,并不总是有永久正确的答案。 始终configuration文件,但也总是以多种方式测量,因为configuration文件不总是准确或可靠的。 除非确实存在明显的问题,否则应避免进行大量的优化。
- 在自动化testing中包含对敏感区域的性能检查,并在浏览器更新时运行可能更好。
- 请记住,有时电池寿命以及可感知的性能。 在运行优化编译器之后,最慢的解决scheme可能会变得更快(IE,编译器可能更好地了解何时访问受限的作用域variables,而不是按照约定标记为私有的属性)。 考虑像node.js这样的后端。 这可能需要比您经常在浏览器上find的更好的延迟和吞吐量。 大多数人不需要担心这些东西,像registry格的validation,但这种事情可能重要的不同场景的数量正在增加。
- 你必须小心内存分配跟踪工具来坚持结果。 在某些情况下,我没有返回并坚持数据完全优化,或者实例化/未引用之间的采样率是不够的,让我挠头如何一个数组初始化和填充到一百万注册为3.4KiB在configuration文件中。
- 在现实世界中,大多数情况下,真正优化应用程序的唯一方法是首先编写它,以便测量它。 有几十到几百个因素可以发挥作用,如果不是在任何情况下数千。 发动机也会做一些事情,导致不对称或非线性的性能特征。 如果你在一个构造函数中定义函数,它们可能是箭头函数或传统函数,在某些情况下每个函数的行为都不一样,我不知道其他的函数types。 对于应该是等价的原型构造函数,类的performance也不一样。 你也需要非常小心基准testing。 原型类可以以各种方式进行延迟初始化,特别是如果你的原型也是你的原型(build议,不要)。 这意味着您可以低估初始化成本并夸大访问/资产突变成本。 我也看到了渐进式优化的迹象。 在这些情况下,我已经填充了一个大对象的实例相同的实例,随着实例的数量增加,对象似乎在内存中进行了增量优化,直到剩余部分相同。 这些优化也可能会显着影响CPU的性能。 这些事情不仅严重依赖于您编写的代码,而且还会在运行时发生诸如对象数量,对象之间的差异等。