【问题标题】:Defining methods via prototype vs using this in the constructor - really a performance difference?通过原型定义方法与在构造函数中使用 this - 真的是性能差异吗?
【发布时间】:2016-10-10 19:28:59
【问题描述】:

在 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. :("); 
}

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

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

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

【问题讨论】:

标签: javascript performance memory-management prototype


【解决方案1】:

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

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

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

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

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

【讨论】:

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

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

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

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

【讨论】:

  • 看起来 jsperf.com 的活动时间更长。你还有其他的性能测量吗?
  • jsPerf 再次启动。 Chrome 55 中的此测试给出了相同的结果,而在 Firefox 50 中使用 this 的速度是原来的三倍。
  • 那个测试是错误的。在第一个中,您实例化该类,然后在每次迭代时调用该方法。在第二个中,您将类实例化一次,然后每次迭代只调用该方法。
【解决方案3】:

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

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

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

【讨论】:

    【解决方案4】:

    你可能没有考虑到这一点,但是将方法直接放在对象上实际上有一个更好的方法:

    1. 方法调用稍微快了 (jsperf),因为不必参考原型链来解析方法。

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

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

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

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


    旁白:

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

    【讨论】:

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

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

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

    希望这会有所帮助。 :)

    【讨论】:

    • 您的链接不再有效。您可以在答案中添加代码来说明您的观点吗?
    【解决方案6】:

    这个答案应该被认为是填补缺失点的其余答案的扩展。结合个人经验和基准。

    就我的经验而言,我使用构造函数来虔诚地构造我的对象,无论方法是否是私有的。主要原因是当我开始时,这对我来说是最简单的直接方法,所以它不是特别偏好。它可能就像我喜欢可见的封装一样简单,并且原型有点脱离实体。我的私有方法也将被分配为范围内的变量。尽管这是我的习惯,并且可以很好地保持事物的独立性,但这并不总是最好的习惯,而且我有时会碰壁。除了根据配置对象和代码布局进行高度动态自组装的古怪场景之外,在我看来,它往往是较弱的方法,特别是在性能受到关注的情况下。知道内部是私有的是有用的,但你可以通过其他方式和正确的纪律来实现这一点。除非性能是一个严肃的考虑因素,否则对手头的任务使用最有效的方法。

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

    免责声明:

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

    【讨论】:

      【解决方案7】:

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

      【讨论】:

      • 但您仍在为每个实例创建一个新方法,因此在此处使用原型不会节省内存。
      • @riscarrott 不,它不是为每个实例创建它。每个实例只调用构造函数。您也可以像这样轻松地检查它:p1.showDetails === p2.showDetails 以证明它是一个功能。
      • 抱歉,误读了。那么用自调用 fn 包装它有什么好处呢?
      • 您立即执行它,以便之后定义 Person 并可供使用。使用这种方法,您也可以定义“静态”方法。基本上由于 JavaScript 没有类,这种方法试图适应这种限制。你可以阅读更多关于它的信息here
      猜你喜欢
      • 2011-10-30
      • 1970-01-01
      • 2011-05-29
      • 1970-01-01
      • 2015-10-30
      相关资源
      最近更新 更多