返回课程

函数大军

重要性: 5

以下代码创建了一个 shooters 数组。

每个函数都应该输出它的数字。但有些地方不对…

function makeArmy() {
  let shooters = [];

  let i = 0;
  while (i < 10) {
    let shooter = function() { // create a shooter function,
      alert( i ); // that should show its number
    };
    shooters.push(shooter); // and add it to the array
    i++;
  }

  // ...and return the array of shooters
  return shooters;
}

let army = makeArmy();

// all shooters show 10 instead of their numbers 0, 1, 2, 3...
army[0](); // 10 from the shooter number 0
army[1](); // 10 from the shooter number 1
army[2](); // 10 ...and so on.

为什么所有射手都显示相同的值?

修复代码,使它们按预期工作。

打开带有测试的沙盒。

让我们仔细看看makeArmy内部究竟发生了什么,解决方案就会变得很明显。

  1. 它创建了一个空数组shooters

    let shooters = [];
  2. 在循环中通过shooters.push(function)用函数填充它。

    每个元素都是一个函数,所以生成的数组看起来像这样

    shooters = [
      function () { alert(i); },
      function () { alert(i); },
      function () { alert(i); },
      function () { alert(i); },
      function () { alert(i); },
      function () { alert(i); },
      function () { alert(i); },
      function () { alert(i); },
      function () { alert(i); },
      function () { alert(i); }
    ];
  3. 数组从函数中返回。

    然后,稍后,对任何成员的调用,例如army[5](),将从数组中获取元素army[5](它是一个函数)并调用它。

    现在为什么所有这些函数都显示相同的值10呢?

    这是因为shooter函数内部没有局部变量i。当调用这样的函数时,它会从其外部词法环境中获取i

    那么,i的值是多少呢?

    如果我们查看源代码

    function makeArmy() {
      ...
      let i = 0;
      while (i < 10) {
        let shooter = function() { // shooter function
          alert( i ); // should show its number
        };
        shooters.push(shooter); // add function to the array
        i++;
      }
      ...
    }

    我们可以看到所有shooter函数都是在makeArmy()函数的词法环境中创建的。但是当调用army[5]()时,makeArmy已经完成了它的工作,i的最终值为10whilei=10处停止)。

    因此,所有shooter函数都从外部词法环境中获取相同的值,即最后一个值i=10

    如上所述,在while {...}块的每次迭代中,都会创建一个新的词法环境。因此,为了解决这个问题,我们可以将i的值复制到while {...}块中的一个变量中,如下所示

    function makeArmy() {
      let shooters = [];
    
      let i = 0;
      while (i < 10) {
          let j = i;
          let shooter = function() { // shooter function
            alert( j ); // should show its number
          };
        shooters.push(shooter);
        i++;
      }
    
      return shooters;
    }
    
    let army = makeArmy();
    
    // Now the code works correctly
    army[0](); // 0
    army[5](); // 5

    这里let j = i声明了一个“迭代局部”变量j并将i复制到其中。基本类型是“按值”复制的,所以我们实际上得到了i的一个独立副本,它属于当前循环迭代。

    射击手正常工作,因为i的值现在离得更近了一点。不在makeArmy()词法环境中,而是在对应于当前循环迭代的词法环境中

    如果我们一开始使用for,也可以避免这样的问题,如下所示

    function makeArmy() {
    
      let shooters = [];
    
      for(let i = 0; i < 10; i++) {
        let shooter = function() { // shooter function
          alert( i ); // should show its number
        };
        shooters.push(shooter);
      }
    
      return shooters;
    }
    
    let army = makeArmy();
    
    army[0](); // 0
    army[5](); // 5

    这本质上是一样的,因为for在每次迭代中都会生成一个新的词法环境,它有自己的变量i。所以每次迭代中生成的shooter都会引用它自己的i,来自那次迭代。

现在,既然你已经付出了如此多的努力来阅读这篇文章,而且最终的方案如此简单——只需使用for,你可能会想——值得吗?

好吧,如果你能轻松地回答这个问题,你就不会阅读解决方案了。所以,希望这个任务能帮助你更好地理解一些事情。

此外,确实存在一些情况下,人们更喜欢使用 while 而不是 for,以及其他情况下,这些问题是真实存在的。

在沙盒中打开带有测试的解决方案。