【问题标题】:Is there a downside to using closures over prototypes?在原型上使用闭包有缺点吗?
【发布时间】:2021-05-07 18:00:46
【问题描述】:

我喜欢使用闭包创建具有私有属性的对象的一般风格。我不确定是在闭包内还是在对象的实际原型上创建原型方法是否更有效。考虑以下示例:

const A = function(a, b) {

  this.a = a;
  this.b = b;

};

A.prototype = {

  add:function() { this.a += this.b; }

};

const B = (function() {

  function add() { this.a += this.b; }

  return function(a, b) {

    return { a, b, add };

  };

})();

示例 A 是一个带有原型的传统构造函数。使用它看起来像这样:

var a = new A(1, 1);
a.add(); // a.a == 2;

示例 B 是使用闭包的技术。使用它看起来像这样:

var b = B(1, 1);
b.add(); // b.a == 2;

我期望 a 的工作方式是 A 的每个实例都有 a 和 b 属性以及指向包含 add 方法的原型对象的指针。

我期望 b 工作的方式是 B 的每个实例都有 a 和 b 属性以及指向 add 方法的指针。 B 函数有一个包含 add 方法的闭包。 add 方法只在定义 B 时定义一次。这似乎是在 JS 中创建“原型”属性和方法的一种不错的方式。有谁知道这是创建具有“共享”属性的对象的高性能方法,还是使用原型的传统方法的可行替代方案?

【问题讨论】:

  • 对不起,我完全误读了你的闭包示例!我猜还没有喝足够的茶... :-)
  • :-) 它绝对不会每次都重新创建add,对此感到抱歉。
  • FWIW,我总是非常对孤立的合成微基准持怀疑态度。它们很少与您的代码实际执行的操作相匹配,这很重要,因为现代 JavaScript 引擎会根据代码的实际使用进行优化(分几个阶段)。此外,您必须经常做超过 1k 次的操作才能让现代 JS 引擎对其进行优化。 :-) 还要注意以较慢的使用速度为代价的更快的创建(因为您使用对象的频率通常比创建它们的频率高)。在this equally suspect benchmark 中,差异要小得多(尽管仍然很大)。
  • 最后,我要提防过早的优化。我不知道这还为时过早,您很可能正在开发一个将定期分配数百万个对象的系统。但是,如果您根据 new 速度慢(而不是出于个人风格原因)的规范执行此操作,我强烈建议您不要这样做。无论如何,玩得开心! :-)
  • 具体用例是在游戏中实例化对象,我可能不会有几百万个对象,但可能有几千个。在主游戏循环期间使用new 实例化对象时存在明显的延迟。这可以通过在循环开始之前进行对象池/实例化来克服,但是这一发现使我走上了这条微优化的道路,然后好奇心接管了。我主要关心的是使用这些对象。它们使用起来会更慢吗?他们肯定会占用更多的内存。我在创建适当的基准来测试这一点时遇到了麻烦。

标签: javascript constructor scope closures lexical-scope


【解决方案1】:

更好的是,不要使用原型或闭包。只需使用常规函数即可。

const C = (a, b) => ({ a, b });

function add(c) {
    c.a += c.b;
}

const c = C(1, 1);
add(c); // c.a === 2;

console.log(c); // { a: 2, b: 1 }

【讨论】:

  • 我倾向于那个方向。它会让我有能力在其他对象上使用这些方法。我可以将我的对象混合在一起,并根据需要在它们上调用适当的函数。您知道这种方法有什么额外的好处吗?
  • 优势。 1) 你避免了thisnew 关键字的所有问题。 2)您的代码非常快。 3)一致的函数调用(即所有函数都被称为bar(foo),而不是一些函数被称为foo.bar()而另一些被称为new Foo())。 4) 更简洁的代码。比较原型和闭包需要多少行代码。
  • 我同意,代码可能会更快,并且它适用于对象与方法分离的设计模式,从而使继承/组合不那么复杂。我一直在研究实体组件系统,很多人使用这种方法来保持模块化。代码行可能不会改进太多,因为方法可能会存储在容器中,因此调用它会更长:container.method(object)。虽然,可以进行本地引用:const method = container.method.
【解决方案2】:

您的闭包示例对于那种风格的构建器函数来说有点不典型,因为它不会在 B 本身中创建任何函数,只是在创建 B 的匿名函数中创建;这意味着它避免了这样做的通常小缺点(每次调用 B 时重新创建函数)。 (通常“闭包风格”在B 本身中创建函数,因此它可以在执行上下文中直接使用ab,而不是使用this.athis.b 属性。)

我看不出你正在做的任何性能下降。从理论上讲,它会有一个非常轻微的性能提升(完全除了您观察到对象字面量比使用new 快得多),因为实例上的方法是自己的属性而不是继承的属性等等(理论上) 它们查找起来会稍微快一些(因为 JavaScript 引擎会立即在对象上找到它们,而不是在对象本身上找不到它们而不得不去查看原型)。但实际上,我希望任何现代 JavaScript 引擎都能进行优化,这样您就不会从中受益。

我可以看到一些与性能无关的缺点,但没有什么大的:

  1. 一切都只是一个对象,所以如果你正在调试一个不相关的性能问题并查看堆快照,一切都只是Object,而不是ABC,这使得使用内存分析器更难。我试图通过向它们添加 constructor 属性来获取内存配置文件以对它们进行分类,但它不起作用(无论如何在 Chrome 中)。我认为它使用了对象原型的构造函数。你可以通过给对象一个特定于构造函数的原型来解决它,只是不使用它,但这似乎有点奇怪。

  2. 要在不使用原型对象的情况下增强对象,您必须改用 mixins,这意味着将所有属性从一个对象复制到另一个对象,这需要更多工作,并且意味着每个对象都比它们更大使用原型继承,可能会在低端设备(手机等)上造成内存流失。 (或者,使用组合而不是增强,这样做有不同的论据。)

  3. 您正在与语言对抗。 JavaScript 的原型本质是它的重要组成部分(即使您不使用构造函数而是使用Object.create 或类似函数)。 JavaScript 引擎旨在擅长优化原型关系。你会做其他事情。它可能很好,只是看起来不太理想。

  4. 当使用的模式不典型时,其他人就更难跟上代码库的速度。使用 JavaScript 的三种典型方式是构造函数(带有原型)、非thisclosure/Object.create 方式(带有原型)和函数式编程。你看到的是前两者的混合体。

但是,如果您要解决与对象创建速度相关的特定问题,那么有针对性地使用这种稍微不寻常的模式可能会非常成功。或者您可能只是更喜欢它,并乐于接受不利的一面。 :-)

【讨论】:

  • 你提出了一些非常好的观点,我同意。这是非典型的,性能提升(如果有的话)可能不值得因为这种模式而出现的其他问题。最终,我想我会在使用它们的对象之外定义一些我的“共享”方法。对象将简单地存储数据,函数将对象作为参数来操作该数据。如果一个对象继承自另一个对象,我就不必担心获取不必要的方法。当我选择应用一个方法时,它可以应用到任何具有相关数据属性的对象。
猜你喜欢
  • 2019-12-30
  • 2016-09-19
  • 2011-09-27
  • 2011-09-12
  • 2010-09-15
  • 1970-01-01
  • 2017-04-03
  • 2011-11-05
  • 1970-01-01
相关资源
最近更新 更多