05-scope和异步执行
14.2.2 scope和异步执行
异步执行中容易让人疑惑或犯错的一点是:scope和闭包是如何影响异步执行的。每当一个函数被调用时,都创建了一个闭包(closure):所有在函数内部创建的变量(包括形参)只在有被访问的时候才存在。
之前见过这个例子,但是由于其中有一些重要的内容需要学习,所以这里再次强调。考虑一个叫作 countdown
的函数的例子,其目的是创造一个5秒的倒计时。
function countdown() {
let i; // 注意这里的let是定义在for循环之外的
console.log("Countdown:");
for(i=5; i>=0; i--) {
setTimeout(function() {
console.log(i===0 ? "GO!" : i);
}, (5-i)*1000);
}
}
countdown();
先在大脑中过一下这个例子。大家可能会记得上一次看到这个例子的时候遇到的错误。该例子看起来是期望从5开始倒计时。但实际上得到的却是6次 −1
,没有一个 "GO!"
。我们第一次看到它的时候,使用的是var;这次则用了 let
,但由于 let
的定义在 for
循环之外,所以会有同样的问题: for
循环执行完毕时, i
的值已经为−1,而之后 callback
函数才开始执行。这里的问题在于,当函数在执行时, i
的值已经被赋为 −1
了。
这里我们需要重要了解的内容是:理解scope和异步执行是如何关联的。当调用 countdown
时,创建了一个包含变量 i
的闭包。所有在for循环中创建的(匿名)回调函数都可以访问 i
,并且是同一个 i
。
本例中有个小细节,就是在 for
循环中,i有两种不同的使用方式。当用它来计算超时( (5−i)*1000
)时,它可以正常工作:第一个超时是0、第二个是 1000
、第三个是 2000
,以此类推。之所以这样是因为计算是同步的。实际上,对 setTimeout
的调用也是同步的(这就需要先进行计算,这样 setTimeout
才知道什么时候调用回调函数)。真正的异步调用是传到 setTimeout
的中的函数,而这才是问题的关键。
还记得之前通过即时调用函数表达式(IIFE)来解决这个问题吗?或者可以用更简单的方式,直接把i的定义挪到 for
循环中。
function countdown() {
console.log("Countdown:");
for(let i=5; i>=0; i--) { // i在块语句的scope中
setTimeout(function() {
console.log(i===0 ? "GO!" : i);
}, (5-i)*1000);
}
}
countdown();
本小节学到的是,必须时刻注意定义回调函数的作用域:回调函数可以访问闭包内的所有内容。正是由于这个原因,回调函数的实际执行结果可能会跟预期的不一样。这个原则适用于所有的异步技术,而不仅仅是回调。