通过原型定义方法vs在构造函数中使用它 - 真的是性能差异?

时间:2012-08-29 14:54:29

标签: javascript performance memory-management prototype

在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的人saying,因为所有实例共享相同的函数副本而不是每个实例都拥有自己的副本。通过原型定义函数有一个巨大的缺点 - 它使得无法拥有私有实例变量。

尽管从理论上讲,使用方法1给对象的每个实例赋予了它自己的函数副本(因此使用了更多的内存,更不用说分配所需的时间) - 这是实际发生的事情吗?似乎优化Web浏览器可以很容易地识别这种非常常见的模式,并且实际上让对象的所有实例引用通过这些“构造函数”定义的函数的相同副本。然后,如果稍后明确更改它,它只能为实例提供自己的函数副本。

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

7 个答案:

答案 0 :(得分:61)

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

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

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

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

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

答案 1 :(得分:2)

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

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

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

答案 2 :(得分:1)

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

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

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

答案 3 :(得分:1)

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

  1. 方法调用非常更快jsperf),因为不需要咨询原型链来解析该方法。
  2. 然而,速度差异几乎可以忽略不计。最重要的是,将方法放在原型上会有两种更有效的方法:

    1. 更快创建实例jsperf
    2. 使用更少内存
    3. 正如詹姆斯所说,如果要实例化数千个类的实例,这种差异可能很重要。

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

      <强> ASIDE:

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

答案 4 :(得分:0)

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

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

希望这会有所帮助。 :)

答案 5 :(得分:0)

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

就我的经验而言,我使用构造函数来虔诚地构建我的对象,无论方法是否是私有的。主要原因是,当我开始时,这对我来说是最简单的方法,所以它不是特别的偏好。它可能就像我喜欢可见的封装一样简单,原型有点无实体。我的私有方法也将被指定为范围中的变量。虽然这是我的习惯,并且保持良好的自我控制,但它并不总是最好的习惯,我有时会碰壁。除了根据配置对象和代码布局进行高度动态自组装的古怪场景之外,在我看来,它往往是较弱的方法,特别是如果性能是一个问题。知道内部是私有的是有用的,但你可以通过其他方式通过正确的规则来实现。除非性能是一个重要的考虑因素,否则请使用最适合手头任务的方法。

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

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

答案 6 :(得分:-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();