当前位置:嗨网首页>书籍在线阅读

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();

本小节学到的是,必须时刻注意定义回调函数的作用域:回调函数可以访问闭包内的所有内容。正是由于这个原因,回调函数的实际执行结果可能会跟预期的不一样。这个原则适用于所有的异步技术,而不仅仅是回调。