var 和 let 在 for循环中的区别(ps:let循环中,块级作用域如何产生以及如何记忆变量i)

515 阅读2分钟

var

var a = [];
for (var i = 0; i < 3; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[0](); //3
a[1](); //3
a[2](); //3

Why? 让我们把这个循环拆开来,等价下面这种形式。

var a = [];
{
  //父级作用域
  var i = 0;
  if (0 < 3) {
    a[0] = function () {
      //函数作用域一
      console.log(i);
    };
  };
  i++; //为1
  if (1 < 3) {
    a[1] = function () {
      //函数作用域二
      console.log(i);
    };
  };
  i++; //为2
  if (2 < 3) {
    a[2] = function () {
      //函数作用域三
      console.log(i);
    };
  };
  i++; //为3
  // 跳出循环
}
//调用N次指向都是最终的3
a[0](); //3
a[1](); //3
a[2](); //3

我们发现在三次循环过程中,一共生成了三个不同的函数作用域,但是变量 i 是 var 声明的,根据作用域链的原理,三个函数作用域中的 i 全都往上寻找,全部指向的是父级作用域中的 i。但是循环到最后,父级作用域中的 i = 3,所以得到上面的结果。

let

var a = [];
for (let i = 0; i < 3; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[0](); //0
a[1](); //1
a[2](); //2

Why? 查阅阮一峰ES6入门中的解释,我们可以得到一个模糊的解释:变量i是let声明的,当前的i只在本轮循环有效,所以每一次循环的i其实都是一个新的变量。你可能会问,如果每一轮循环的变量i都是重新声明的,那它怎么知道上一轮循环的值,从而计算出本轮循环的值?这是因为 JavaScript 引擎内部会记住上一轮循环的值,初始化本轮的变量i时,就在上一轮循环的基础上进行计算。

我们从这里可以引出两个问题:

  1. “当前的i只在本轮循环有效,所以每一次循环的i其实都是一个新的变量”,这说明所有的 i 是相互独立的,分别有各自独立的作用域(如果不是有各自独立的作用域,那么 i 就不会相互独立了),但是在花括号中并没有使用 let(关键字let会导致块级作用域),那么每次循环时候,各自独立的作用域是怎么产生的呢?
  2. “ JavaScript 引擎内部会记住上一轮循环的值,初始化本轮的变量i时,就在上一轮循环的基础上进行计算。”初始化本轮 i 到底是怎么样实现的呢?

最开始,我也很迷惑这两个问题,后来看了一篇博客,整理如下:

// 全局作用域
  var a = []; 
  {
    // 父级作用域
    let i = 0;
    if (i < 3) {
        // 块级作用域一
        let k = i;  //这一步模拟底层实现,let关键字导致形成了块级作用域,let k = i,使得每次循环的时候记住上一次循环时候的值
        a[k] = function () { //之后花括号后面的操作都是引用 k ,而不是 i
            // 函数作用域一
            console.log(k); //之后花括号后面的操作都是引用 k ,而不是 i
        };
    };
    i++; //为1
    if (i < 3) {
        // 块级作用域二
        let k = i;
        a[k] = function () {
            // 函数作用域二
            console.log(k);
        };
    };
    i++; //为2
    if (i < 3) {
        // 块级作用域三
        let k = i;
        a[k] = function () {
            // 函数作用域二
            console.log(k);
        };
    };
    i++; //为3
    // 跳出循环
}
a[0](); //0
a[1](); //1
a[2](); //2

解释:let的每次循环过程中,在花括号里面,JavaScript 引擎内部首先会执行类似的 "let k = i; "这样的操作,let关键字导致形成了块级作用域,let k = i,同时使得每次循环的时候记住上一次循环时候的值。然后每个对应的函数作用域在引用变量的时候沿着作用域链往上寻找,找到对应的块级作用域中声明的 k,这样就会得到上面的结果。