「前端每日一问(34)」闭包和循环陷阱

1,871 阅读3分钟

Offer 驾到,掘友接招!我正在参与2022春招打卡活动,点击查看活动详情

本题难度:⭐ ⭐

执行下面这段代码,输出的结果是什么?

const arr = []
for (var i = 0; i < 5; i++) {
  arr.push(function () {
    console.log(i)
  })
}

arr[0]()
arr[1]()
arr[2]()

你可能会脱口而出,这么简单,输出 0,1,2。

但很遗憾,事实并非如此,最终输出结果为 5,5,5,如下图:

image.png

原因是在 for 循环里定义变量 i,其实就相当于在全局定义了一个变量 i。

for (var i = 0; i < 5; i++) {
  // ...
}

// 这两种写法完全等价

var i = 0
for (; i < 5; i++) {
  // ...
}

程序运行时,for 循环一瞬间就结束了,在打印执行之前,i 就已经变成 5 了,最后执行时输出的都是 5。

const arr = []
var i = 0
for (; i < 5; i++) {
  arr.push(function () {
      console.log(i)
  })
}

console.log('i :>> ', i) // 输出5,在打印执行前,输出 i,就已经是 5 了
console.log('window.i :>> ', window.i); // 输出5,访问 i 其实就相当于访问 window.i,也是5

arr[0]() // 5
arr[1]() // 5
arr[2]() // 5

那么,如何按顺序打印出 0,1,2,3,4 呢?

其实只需要把每次执行 for 循环的 i 存起来,就能解决全部输出 5 的问题了。

有三种思路来解决循环陷阱:

  • 使用闭包存值。
  • 使用块级作用域存值。
  • 封装一个函数,把每次循环的值传递过去

使用闭包解决循环陷阱问题

还记得闭包怎么实现的吗,在函数内部定义私有变量,并想办法在外部访问这个私有变量,就可以这么写:

const arr = []
for (var i = 0; i < 5; i++) {
  const fn = function (n) { // 定义闭包函数
    arr.push(function () {
      console.log(n)
    })
  }
  fn(i) // 执行函数,把每次执行 for 循环的 i 传进去
}

arr[0]() // 0
arr[1]() // 1
arr[2]() // 2

这样就可以把每次执行 for 循环的 i 存在闭包里

image.png

也可以用立即执行函数来写,就可以直接写匿名函数,少写点代码:

const arr = []

for (var i = 0; i < 5; i++) {
  (function (n) { // 立即执行函数
    arr.push(function () {
      console.log(n)
    })
  })(i) // 把每次执行 for 循环的 i 传进去
}

arr[0]() // 0
arr[1]() // 1
arr[2]() // 2

效果是一模一样的

image.png

拓展:解决循环陷阱的其他方式

使用块级作用域存值

其实还有一种更简单的写法,可以实现按顺序输出 0,1,2,3,4,就是使用 let 来定义变量 i,把 i 的值存进块级作用域里,代码如下:

const arr = []
for (let i = 0; i < 5; i++) {
  arr.push(function () {
    console.log(i)
  })
}

arr[0]() // 0
arr[1]() // 1
arr[2]() // 2

image.png

函数传参解决

其实还有一种方法可以解决这个问题,而且比上面的解决方案都符合直觉,

const arr = []
for (var i = 0; i < 5; i++) {
  print(i)
}

function print(i) {
  arr.push(function () {
    console.log(i)
  })
}

arr[0]() // 0
arr[1]() // 1
arr[2]() // 2

利用的原理就是原始数据类型作为参数传入函数时,是按值传递的,就可以把每次的 i 值准确地传到 print 函数里。

使用 bind 函数

上面的例子就是封装了一个 print 函数传参来解决,其实也可以不用专门封装,可以用 bind 函数来解决。

bind 函数可以显式改变 this 指向,会返回一个新的函数,我们利用 bind 函数会返回一个新函数的特性,来解决循环陷阱的问题。

const arr = []
for (var i = 0; i < 5; i++) {
  arr.push(function (n) {
    console.log(n)
  }.bind(this, i)) // 不用改变 this 指向,我们主要是为了返回一个新的函数
}

arr[0]() // 0
arr[1]() // 1
arr[2]() // 2

结尾

关于闭包有不理解的,可以看我的这篇文章:

「前端每日一问(28)」说说你对闭包的理解

如果我的文章对你有帮助,你的👍就是对我的最大支持^_^

你也可以关注《前端每日一问》这个专栏,防止失联哦~

我是阿林,输出洞见技术,再会!

上一篇:

「前端每日一问(33)」闭包与柯里化函数、偏函数的关系?

下一篇;

「前端每日一问(35)」webpack 模块化打包是如何用闭包实现的?