如果回调是在定义循环的同一范围内定义的(这种情况经常发生),则回调将可以访问索引变量。暂时抛开 NodeJS 的细节,让我们考虑一下这个函数:
function doSomething(callback) {
callback();
}
该函数接受一个回调函数引用,它所做的就是调用它。不是很令人兴奋。:-)
现在让我们在循环中使用它:
var index;
for (index = 0; index < 3; ++index) {
doSomething(function() {
console.log("index = " + index);
});
}
(在计算密集型代码中——比如服务器进程——最好不要在生产代码中逐字执行上述操作,我们稍后会回到这一点。)
现在,当我们运行它时,我们会看到预期的输出:
index = 0
index = 1
index = 2
我们的回调能够访问index
,因为回调是对其定义范围内的数据的闭包。(不要担心术语“闭包”,闭包并不复杂。)
我说最好不要在计算密集型生产代码中逐字执行上述操作的原因是代码在每次迭代时都会创建一个函数(除非在编译器中进行花哨的优化,V8 非常聪明,但是优化创建这些函数是非平凡)。所以这里有一个稍微修改的例子:
var index;
for (index = 0; index < 3; ++index) {
doSomething(doSomethingCallback);
}
function doSomethingCallback() {
console.log("index = " + index);
}
这可能看起来有点令人惊讶,但它仍然以相同的方式工作,并且仍然具有相同的输出,因为doSomethingCallback
仍然是一个闭包index
,所以它仍然看到index
调用它时的值。但是现在只有一个doSomethingCallback
函数,而不是每个循环都有一个新函数。
现在,让我们一个反面的例子,一些不工作:
foo();
function foo() {
var index;
for (index = 0; index < 3; ++index) {
doSomething(myCallback);
}
}
function myCallback() {
console.log("index = " + index); // <== Error
}
那失败了,因为myCallback
没有在定义的同一范围(或嵌套范围)index
中定义,因此index
在myCallback
.
最后,让我们考虑在循环中设置事件处理程序,因为必须小心处理。在这里,我们将深入了解 NodeJS:
var spawn = require('child_process').spawn;
var commands = [
{cmd: 'ls', args: ['-lh', '/etc' ]},
{cmd: 'ls', args: ['-lh', '/usr' ]},
{cmd: 'ls', args: ['-lh', '/home']}
];
var index, command, child;
for (index = 0; index < commands.length; ++index) {
command = commands[index];
child = spawn(command.cmd, command.args);
child.on('exit', function() {
console.log("Process index " + index + " exited"); // <== WRONG
});
}
它看起来像上面应该工作相同的方式,我们前面的循环一样,但有一个关键的区别。在我们之前的循环中,回调被立即调用,因此它看到了正确的index
值,因为index
还没有机会继续。但是,在上面,我们将在调用回调之前旋转循环。结果?我们看
Process index 3 exited
Process index 3 exited
Process index 3 exited
这是一个关键点。闭包没有它关闭的数据的副本,它有一个对它的实时引用。因此,当exit
每个进程的回调运行时,循环已经完成,因此所有三个调用都会看到相同的index
值(它在循环结束时的值)。
我们可以通过让回调使用一个不会改变的不同变量来解决这个问题,如下所示:
var spawn = require('child_process').spawn;
var commands = [
{cmd: 'ls', args: ['-lh', '/etc' ]},
{cmd: 'ls', args: ['-lh', '/usr' ]},
{cmd: 'ls', args: ['-lh', '/home']}
];
var index, command, child;
for (index = 0; index < commands.length; ++index) {
command = commands[index];
child = spawn(command.cmd, command.args);
child.on('exit', makeExitCallback(index));
}
function makeExitCallback(i) {
return function() {
console.log("Process index " + i + " exited");
};
}
现在我们输出正确的值(无论进程退出的顺序如何):
Process index 1 exited
Process index 2 exited
Process index 0 exited
工作的方式是我们分配给exit
事件的回调在我们i
调用的参数上关闭makeExitCallback
。第一回调makeExitCallback
创建并返回关闭在i
该呼叫到值makeExitCallback
,它在创建关闭第二回调i
为值该呼叫makeExitCallback
(这是比不同i
的先前调用值)等。
如果你阅读上面链接的文章,很多事情应该更清楚。文章中的术语有点过时(ECMAScript 5 使用更新的术语),但概念没有改变。