前置知识: 作用域链

概念汇总

「JavaScript」闭包 - 图1

经典应用

循环会丢失当前值

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

上文代码,理想情况是输出1 2 3 4 5,但实际上大家都知道,会输出5个6。
这是因为 i 的当前作用域与 for 循环一样,他们共享一个全局作用域,也即是 i 指向一个全局的引用。
这里我们可能会想当然的认为在每次循环的时候,全局的 i 的值也并不是6呀。这里需要提一下 setTimeout:js使用回调函数的方式实现多任务,setTimeout 的回调函数只会在循环结束后被统一调用,而这时拿到的肯定是6。

闭包解法

好,这里再复习一下思维导图里闭包的特性:每个函数的作用域是自己定义时的作用域,那么我们只要在每次循环的时候定义一个闭包,将 i 当时的值保存进去不就好了。
根据这个思路,我们写出下面↓的这个升级版:

  1. for(var i = 1; i<=5;i ++) {
  2. (function() {
  3. var j = i
  4. setTimeout(function() {
  5. console.log(j)
  6. }, 1000)
  7. })()
  8. }

此时一共有三个作用域,for 和 i 所在的全局作用域,包含 j 和 setTimeout的 立即执行函数(IIFE) 所声明出的作用域,包含 console.log 的匿名函数A的作用域。此时 IIFE函数 和 匿名函数A 都是闭包。
在匿名函数A中,寻找变量j的声明,沿着作用域链向上找,发现在IIFE中声明值为 i ,IIFE函数同样是闭包,在其声明时,i 即为当时的 i 值,也就是 1、2、3、4、5,将这个值传给匿名函数A。
由此,可以输出想要的 1、2、3、4、5。
但是这个写法可以再优化一下,同样的内容,将 i 作为一个参数传入 IIFE,减少临时变量的数量,就变成了我们最常见的写法:

  1. for(var i = 1; i<=5;i ++) {
  2. (function(i) {
  3. setTimeout(function() {
  4. console.log(i)
  5. }, 1000)
  6. })(i)
  7. }

块作用域解法

在 ES6 之前的JS中,只有函数、try/catch、with 可以形成块作用域,而我们常用的if、for等并不会,也就是说,在里面用var声明的变量会是其的父作用域

  1. if (true) {
  2. var a = '111'
  3. }
  4. console.log(a) // 111,并没有报错

而在ES6中引入了新的关键字 let ,let关键字可以将变量绑定到所在的任意作用域中(通常是{ .. }内部)。换句话说,let为其声明的变量隐式地劫持了所在的块作用域。

  1. if (true) {
  2. let a = '111'
  3. }
  4. console.log(a) // Uncaught ReferenceError

有了块作用域,其实就可以直接在循环中中大胆声明了,因为每个块作用域中的i指向当前循环被声明的i

  1. for(let i = 1; i<=5;i ++) {
  2. setTimeout(function() {
  3. console.log(i)
  4. }, 1000)
  5. }
  6. // 1 2 3 4 5