函数大军
以下代码创建了一个 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
内部究竟发生了什么,解决方案就会变得很明显。
-
它创建了一个空数组
shooters
let shooters = [];
-
在循环中通过
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); } ];
-
数组从函数中返回。
然后,稍后,对任何成员的调用,例如
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
的最终值为10
(while
在i=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
,以及其他情况下,这些问题是真实存在的。