62

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

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

4

7 回答 7

63

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

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

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

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

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

于 2012-08-29T15:00:10.810 回答
2

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

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

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

于 2015-11-26T03:50:05.633 回答
1

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

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

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

于 2014-01-30T00:39:54.553 回答
1

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

  1. 方法调用要快得多jsperf ),因为不必参考原型链来解析方法。

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

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

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

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


在旁边:

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

于 2015-09-05T20:45:38.200 回答
0

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

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

希望这可以帮助。:)

于 2012-08-29T15:01:48.797 回答
0

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

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

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

免责声明:

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

您可以使用这种方法,它将允许您使用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();
于 2020-03-25T21:15:39.820 回答