6

假设我们正在编写一个浏览器应用程序,其中流畅的动画至关重要。我们知道垃圾收集会阻塞执行足够长的时间以导致可察觉的冻结,因此我们需要尽量减少我们创建的垃圾量。为了尽量减少垃圾,我们需要在主动画循环运行时避免内存分配。

但是该执行路径到处都是循环:

var i = things.length; while (i--) { /* stuff */ }

for (var i = 0, len = things.length; i < len; i++) { /* stuff */ }

他们的var语句allocate memory可以分配垃圾收集器可能删除的内存,这是我们想要避免的。

那么,在 JavaScript 中编写循环结构以避免每个循环结构分配内存的好策略是什么?我正在寻找一个通用的解决方案,列出了利弊。


以下是我提出的三个想法:

1.)声明索引和长度的“顶级”变量;到处重复使用它们

我们可以在顶部声明app.iapp.length,并一次又一次地重用它们:

app.i = things.length; while (app.i--) { /* stuff */ }

for (app.i = 0; app.i < app.length; app.i++) { /* stuff */ }

优点:很容易实现。缺点:取消引用属性对性能的影响可能意味着代价高昂的胜利。可能会意外滥用/破坏属性并导致错误。

2.)如果数组长度已知,不要循环——展开

我们可以保证一个数组有一定数量的元素。如果我们事先知道长度是多少,我们可以在程序中手动展开循环:

doSomethingWithThing(things[0]);
doSomethingWithThing(things[1]);
doSomethingWithThing(things[2]);

优点:高效。缺点:在实践中几乎不可能。丑陋的?改变烦心?

3.)通过工厂模式利用闭包

编写一个返回“looper”的工厂函数,该函数对集合(a la _.each)的元素执行操作。Looper 在创建的闭包中保留对索引和长度变量的私有引用。循环器必须重置i,并且length每次调用它。

function buildLooper() {
  var i, length;
  return function(collection, functionToPerformOnEach) { /* implement me */ };
}
app.each = buildLooper();
app.each(things, doSomethingWithThing);

优点:更实用,更惯用?缺点:函数调用会增加开销。闭包访问被证明比对象查找慢。

4

1 回答 1

1

他们的 var 语句 allocate memory 可以分配垃圾收集器可能删除的内存,这是我们想要避免的。

这有点误导。简单地使用var不会在堆上分配内存。调用函数时,函数中使用的每个变量都预先分配在堆栈上。当函数完成执行时,堆栈帧被弹出并立即取消引用内存。

当您在堆上分配对象时,与垃圾收集相关的内存问题成为问题的地方。这意味着以下任何一项:

  • 闭包
  • 事件监听器
  • 数组
  • 对象

在大多数情况下,任何typeof foo返回"function""object"(或任何新的 ES6typeof返回值)都会在堆上生成一个对象。我现在想不到的可能还有更多。

关于堆上的对象的事情是它们可以引用堆上的其他对象。例如:

var x = {};
x.y = {};
delete x;

在上面的示例中,浏览器根本无法为 释放插槽x,因为其中包含的值是可变大小的。它位于堆上,然后它可以指向其他对象(在这种情况下,对象位于x.y)。另一种可能性是对同一对象的第二次引用:

var x = {};
window.foo = x;
delete x;

浏览器根本无法x从内存中删除对象,因为其他东西仍然指向它。

长话短说,不要担心删除变量,因为它们工作得很好并且完全有性能。堆分配是垃圾回收的真正敌人,但即使是一些小的堆分配也不会伤害大多数应用程序。

于 2014-10-25T22:09:04.967 回答