Javascript 闭包杂谈

闭包

什么是闭包,我自己的理解是能够访问另一个函数作用域的变量的函数,这个描述不一定准确,但方便理解

或许《你不知道的 JavaScript》书中的描述更为准确:当函数可以记住并访问所在的词法作用域,即使函数是在当前词法作用域之外执行,这时
就产生了闭包

下面这段代码就是一个闭包

function foo () {
  var a = 2
  function bar () {
    console.log(a)
  }
  return bar
}
var baz = foo()
baz() // 2

上面函数 bar() 的词法作用域能够访问 foo() 的内部作用域, 但是 bar 却在自己定义的词法作用域以外的地方执行

在 foo() 执行后,一般 foo() 的整个内部作用域都会被垃圾回收机制回收,但闭包却可以阻止这件事情的发生,因为 foo 的词法作用于仍然被 bar 占用

平时书写代码的过程中,处处都有闭包的影子,比如

function foo () {
  var a = 2
  function baz () {
    console.log( a ) // 2
  }
  bar(baz)
}
function bar (fn) {
  fn() // 闭包了
}

再比如

function wait(message) {
  setTimeout(function timer () {
    console.log(message)
  }, 1000 )
}
wait( "Hello, closure!" )

wait() 执行 1000 毫秒后,它的内部作用域并不会消失,timer 函数依然保有 wait() 作用域的闭包

……

循环和闭包

考虑下面的代码

for (var i=1; i<=5; i++) {
  setTimeout(function timer () {
    console.log(i)
  }, i * 1000)
}

正常情况下,我们对这段代码行为的预期是分别输出数字 1~5,每秒一次,每次一个,
但实际上,这段代码在运行时会以每秒一次的频率输出五次 6,因为延迟函数的回调会在循环结束时才执行

想实现预期的效果,可以在循环中加个闭包

for (var i=1; i<=5; i++) {
  (function () {
    var j = i
    setTimeout(function timer () {
      console.log(j)
    }, j * 1000)
  })()
}

在迭代内使用 IIFE 会为每个迭代都生成一个新的作用域,每个迭代中都会含有一个具有正确值的变量供我们访问

上面的代码还可进行一些优化

for (var i=1; i<=5; i++) {
  (function (j) {
    setTimeout(function timer () {
      console.log(j)
    }, j * 1000)
  })(i)
}

拓展(达到预期更屌的方法)

对前面的解决方案进行分析,使用 IIFE 在每次迭代时都会创建一个新的作用域,换句话说,每次迭代我们都需要一个块作用域,所以可以使用 let 来劫持这个块作用域

for (var i=1; i<=5; i++) {
  let j = i // 劫持块作用域
  setTimeout(function timer () {
    console.log(j)
  }, j * 1000)
}

是不是相当的 nice,还没完

for 循环头部的 let 声明还会有一个特殊的行为,这个行为指出变量在每次循环中都会重新声明,因此代码还能简化为

for (let i=1; i<=5; i++) {
  setTimeout(function time () {
    console.log(i)
  }, i * 1000)
}
除特殊说明外本人博客均属原创,转载请注明出处:http://blog.johnhan.cn/blog_1072.html
京ICP备19044523号-1