JavaScript - 少量使用闭包?

21

我正在观看http://www.youtube.com/watch?v=mHtdZgou0qU,在大约13:37(呵呵),他展示了一个避免使用的事物列表,因为新对象添加到作用域链上。

我理解他所说的usingtry-catch语句,以及访问超出作用域的变量,但我不明白为什么应该避免闭包。如果闭包的本地变量将位于作用域链的顶部,那么哪里会有性能损失?


3
请记住:过早地优化是万恶之源。该视频讨论如何使 JavaScript 更具性能,但大多数情况下,JavaScript 不需要进行优化。如果您在用户点击按钮后异步设置函数调用,则用户不会注意到响应速度从 30 毫秒变为 40 毫秒的差异。 - zzzzBov
1
这里的答案都很好。不过请记住,这个视频已经几年了,可能不像以前那样相关了。 - Dave Newton
2个回答

20

这是因为,为了查找非局部变量,虚拟机必须沿着作用域链向上查找它们。另一方面,局部变量被缓存,所以局部变量的查找速度要快得多。函数嵌套越深,作用域链就越长,可能的性能影响也就越大。

这就是为什么你经常会在流行的JS库中看到像这样的代码:

(function(window, document, undefined) {
  // ...
})(window, document);

在这里,“window”和“document”成为本地变量,因此查找它们变得更快,如果您从代码内部引用这些对象数千次,则这一点会变得非常明显。

这个页面详细描述了作用域链和执行上下文。(如果你有时间阅读整篇文章的话,整篇文章也很有趣。)

尽管如此,现代浏览器将所有这些优化到几乎可以忽略的程度,所以这不是我需要担心的事情。


这是正确的,因为闭包存储对局部变量的引用而不是副本,所以它强制进行堆栈遍历而不是局部查找。 - Russ Clarke
我的意思是一个不访问超出作用域变量的闭包。他明确地说要避免访问超出作用域的变量,然后再明确地说要避免使用闭包,这似乎有点多余。暗示着除了使用本地变量外,你还应该避免使用闭包。我是否误解了他的意思? - mowwwalker
2
@Walkerneo:如果您没有引用非本地变量,那么这不是闭包。他的意思是尽可能避免使用闭包,但当您必须引用超出范围的变量时,应使用本地变量以加快查找速度(如果您经常从该函数内部使用该变量-否则它不会有太大作用)。 - Sasha Chedygov
1
我认为你展示的那个模式更多是为了防止一个愚蠢的用户覆盖重要的全局常量,而不是为了性能问题。 - hugomg
@RobG:关于作用域链是静态的这一点是正确的,但我认为浏览器只是最近(相对而言)才开始将它们视为静态的。我记得几年前这仍然被认为是一个非常重要的事情。现在可能已经无关紧要了。 - Sasha Chedygov
显示剩余4条评论

9

这个视频解释了闭包为什么在大约11:08开始会有一些性能问题。

基本上,他说对于每个嵌套函数,它都会向作用域链中添加另一个对象,因此访问闭包外的变量将需要更长的时间。

要查找与变量关联的值,Javascript 解释器会执行以下过程:

  1. 搜索本地作用域对象
  2. 如果第1步不管用,则搜索父级作用域对象
  3. 如果第2步不管用,则搜索父级的父级作用域对象
  4. 继续搜索父级作用域,直到
  5. 搜索全局作用域
  6. 如果还没有找到,则抛出未定义变量错误。

在普通函数中,要查找一个变量,只需要在作用域链的顶部进行搜索。但是,闭包要查找父级变量,则必须向下搜索作用域链,有时要搜索数层深度。例如,如果您有像这样的一些闭包(请注意,这是一个非常牵强附会的例子):

function a (x) {
  function b (y) {
    return (function (z) {
      return x + y + z;
    })(y + y);
  }
  return b(x + 3);
}

从最内层函数开始,为了计算表达式x + y + z,它必须在作用域链中向上遍历三个级别才能找到x,然后再次向上遍历两个级别才能找到y,最后再向上一次才能找到z。总共需要搜索六个对象才能返回最终结果。
这在闭包中是不可避免的,因为闭包总是要访问父变量。如果它们不这样做,使用闭包就没有任何意义。
另外请注意,在Javascript中,创建函数(特别是闭包)有很大的开销。例如,考虑这个相当简单的闭包:
function a(x) {
  return function (y) {
    return x + y;
  }
}

您需要以不同的方式多次调用它,如下:

var x = a(1);
var y = a(2);
var z = a(3);
alert(x(3)); // 4
alert(y(3)); // 5
alert(z(3)); // 6

注意,从a返回的函数必须保留已传递给父函数的参数,即使父函数已经被调用。这意味着解释器必须在内存中保存您到目前为止调用的每个函数中传递的参数。


yz 存储为第三个函数的本地变量是否仍会降低性能?当然,对于这个示例,没有创建闭包的理由,但是如果有呢? - mowwwalker
@Walkerneo 它仍然需要向上查找作用域链以找到父变量。如果您在内部函数中多次使用父变量,则值得将其声明为局部变量,但如果您只使用它们一次,则将它们声明为局部变量没有任何帮助。 - Peter Olson
好的,我理解了遍历作用域链的问题,但是就像我在另一个答案的评论中所说的那样,他在谈到超出作用域的变量之后立即提出“谨慎使用闭包”的方式使得它看起来像闭包本身存在问题,而不仅仅是它们向作用域链中添加了另一个对象。 - mowwwalker
@Walkerneo:如果您观看Peter所引用的视频部分,他会具体讲解我们正在谈论的内容:作用域链。这就是闭包的问题所在。然而,在实际代码中,闭包显然是不可避免的,因此他建议您为每个超出作用域的变量声明一个本地变量,以加快未来的查找速度。 - Sasha Chedygov
1
@Walkerneo,闭包本身并没有什么问题——在我看来,它们绝对是非常棒的东西。只是它们会带来一些性能开销的问题。 - Peter Olson

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接