循环与闭包 之 for循环经典问题解释 / 结合《你不知道的JS》与《高程》案例

案例一

for (var i = 1; i <= 5; i++) {
  setTimeout( function timer() {
    console.log(i);
  }, i*1000)
}

输出结果:

  • 当时间是固定的数,如0、1000、6000,执行结果就是0、1、6秒后,一次输出五个6;
  • 当时间是 i*1000, 输出是:每隔1秒,输出一个6,共5次。

代码中到底有什么‘缺陷’,导致它的行为与 语义暗示的不一致呢?

缺陷 是 我们试图假设
循环中的每一个迭代在运行时,都会给自己’捕获’一个i的副本。但是,根据作用域的工作原理,实际情况是,尽管循环中的五个函数是在各个迭代中分别定义的,但是它们都被封闭在一个共享的全局作用域中,因此实际上只有一个 i。

这样说的话,当然所以函数共享一个 i 的引用。
循环结构让我们误以为背后还有更复杂的机制在起作用,实际上并没有。如果将延迟函数的回调重复定义5次,完全不使用循环,那它同这段代码是完全等价的。

                             ----《你不知道的JS 上卷》p49

show me the code

第一种情况

setTimeout()第二个参数是个常数

书中那段话就是说,

for (var i = 1; i <= 5; i++) {
  setTimeout( function timer() {
    console.log(i);
  }, 1000)
}

等价于

// 注:执行栈内循环需要先结束

var i = 1;
// 定时器将其回调函数加入任务列表,执行栈清空一秒后执行
var i = 2;
// 定时器将其回调函数加入任务列表,执行栈清空一秒后执行
var i = 3;
// 定时器将其回调函数加入任务列表,执行栈清空一秒后执行
var i = 4;
// 定时器将其回调函数加入任务列表,执行栈清空一秒后执行
var i = 5;
// 定时器将其回调函数加入任务列表,执行栈清空一秒后执行
var i = 6;
// 定时器将其回调函数加入任务列表,执行栈清空一秒后执行

// 循环结束后,线程读取任务列表,将定时事件对应的异步任务(回调函数)入栈执行
console.log(i);
console.log(i);
console.log(i);
console.log(i);
console.log(i);
console.log(i);

输出情况:
当时间是固定的数,如0、1000、6000,执行结果就是0、1、6秒后,一次输出五个6;

等到这些回调执行时,i的值就是6

第二种情况

setTimeout()第二个参数含有变量

同上

for (var i = 1; i <= 5; i++) {
  setTimeout( function timer() {
    console.log(i);
  }, i*1000)
}

等价于


var i = 1;
// 定时器将其回调函数加入任务列表,执行栈清空一秒后执行
var i = 2;
// 定时器将其回调函数加入任务列表,执行栈清空二秒后执行
var i = 3;
// 定时器将其回调函数加入任务列表,执行栈清空三秒后执行
var i = 4;
// 定时器将其回调函数加入任务列表,执行栈清空四秒后执行
var i = 5;
// 定时器将其回调函数加入任务列表,执行栈清空五秒后执行
var i = 6;
// 定时器将其回调函数加入任务列表,执行栈清空六秒后执行

// 循环结束后,线程读取任务列表,将定时事件对应的异步任务(回调函数)入栈执行
console.log(i);

console.log(i);

console.log(i);

console.log(i);

console.log(i);

console.log(i);

输出情况:
当时间是 i*1000, 输出是:每隔1秒,输出一个6,共5次。

因为等到这些回调执行时,i的值就是6

HOW TO FIX IT

我知道,闭包!

for (var i = 1; i <= 5; i++) {
  (function() {
    setTimeout( function timer() {
      console.log(i);
    }, i*1000)
  })()
}

然并卵,还是一秒一个6,五次

因为,新加上的 IIFE(创建并立即执行) 作用域是”空的”,它并没有自己的变量。执行栈清空后,线程从任务队列里读取回调函数,它们还是引用那个唯一的全局变量i。

正确的闭包姿势:

通过在闭包作用域中添加自己的变量,从而在每次迭代中,捕获i的副本。

for (var i = 1; i <= 5; i++) {
  (function() {
    var j = i
    setTimeout( function timer() {
      console.log(j);
    }, j*1000)         //至于时间这里,是i 是j无所谓
  })()
}

更简洁的姿势:

for (var i = 1; i <= 5; i++) {
  (function(j) {
    setTimeout( function timer() {
      console.log(j);
    }, j*1000)
  })(i) 
}

由此,能够输出:1 2 3 4 5,一秒一个

ES6的打开方式: 块作用域

for (var i = 1; i <= 5; i++) {

    let j = i
    setTimeout( function timer() {
      console.log(j);
    }, j*1000)

}

或直接在for循环头部里,每次迭代都声明一次

for (let i = 1; i <= 5; i++) {

    setTimeout( function timer() {
      console.log(i);
    }, i*1000)

}

案例2

《JavaScript高级程序设计第三版》 p181

var a = function ceateFunctions() {
  var result = new Array();

  for (var i = 0; i < 10; i++) {
    result[i] = function () {
      return i
    }
  }

  return result
}
console.log(a()[0]());
//0 - 9 输出都是10  

道理一样。

都是因为引用同一个i,且没能在迭代中捕获i的副本,或者没能在迭代中及时按当时的值执行。直到i早都变成10了,才执行,RHS引用的结果当然是i此刻的值,即10。

闭包处理:

var a = function ceateFunctions() {
  var result = new Array();

  for (var i = 0; i < 10; i++) {
  // 创建匿名函数,并立即执行之,将执行结果赋值给数组
    result[i] = (function(num) {
      return function() {
        return num;
      };
    })(i);
  }

  return result;
};
console.log(a()[0]());

相关文章: 单线程 JavaScript 的异步机制与经典 for 循环面试题

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值