for循环
for循环还有一个特别之处,就是设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域。
for (let i = 0; i < 3; i++) {
console.log(i)//error let i = 'abc'; console.log(i); } // abc // abc // abc
上面代码正确运行,输出了 3 次abc。这表明函数内部的变量i与循环变量i不在同一个作用域,有各自单独的作用域。
for循环里的定时器:
for(var i = 0; i < 5; i++) { setTimeout(function () { console.log(i); }); } console.log('a');
答案:
a
5
5
5
5
5
为什么会先打印出a,呢
因为js的任务队列里,除了放置异步操作之外,还会放置定时器事件。
当js代码运行到有定时器的地方的时候,会把定时器放在任务队列的尾部,然后跟他说:“你先排队吧,还没有轮到你,因为同步代码还没有执行完。”
这里的同步代码就是console.log('a')。
总结:定时器并不是同步的,它会自动插入任务队列,等待当前文件的所有同步代码和当前任务队列里的已有事件全部运行完毕之后才能执行。
这就是为什么字符串a再五个5之前打印出来的原因。
那么为什么是5个5呢?为什么不是0,1,2,3,4,
这是因为在所有同步代码执行完毕之后,for循环里的i值早已经变成了5,循环已经结束。(注意,for循环的圆括号部分也是同步代码)
这段代码的真实运行情况可以这样理解:
for(var i = 0; i < 5; i++) { } console.log('a'); setTimeout(function () { console.log(i); }); setTimeout(function () { console.log(i); }); setTimeout(function () { console.log(i); }); setTimeout(function () { console.log(i); }); setTimeout(function () { console.log(i); }); //先循环,i变成了5,然后打印a,然后再打印5次i //这里只是假想,便于理解
作用域和闭包
如果想要在for循环里面的定时器打印出0,1,2,3,4,而不是五个5,该怎么办?
可以使用立即执行函数
for(var i = 0; i < 5; i++) { (function(i) {//这个匿名函数生成了闭包的效果,新建了一个作用域,这个作用域接收到每次循环的i值
//保存了下来,即使循环结束,闭包形成的作用域也不会被销毁 setTimeout(function () { console.log(i); }); })(i) } console.log('a');
结果:
a
0
1
2
3
4
这又是为什么呢?
这是因为for循环里定义的变量i其实暴露在全局作用域呢,于是5个定时器里的匿名函数他们其实共享同一个作用域中的同一个变量。
解决思路:如果想要0,1,2,3,4的结果,就要在每次循环的时候,把当前的i值单独保存下来,怎么存下当前的循环值???
利用闭包的原理,闭包使一个函数可以继续访问它定义时的作用域。而这个新生成的作用域将每一次循环的当前i值保存下来。
let关键字、块作用域以及try...catch语句
for(let i = 0; i < 5; i++) { setTimeout(function () { console.log(i); }); }
注意for循环定义i的时候把var换成了let,打印出的结果就是0,1,2,3,4
这是问什么呢?
因为let关键字劫持了for循环的块作用域,产生了类似闭包的效果。并且在for循环中使用let来定义循环变量还会有一个特殊效果:每一次循环都会重新声明变量i,随后的每个循环都会使用上一个循环结束时的值来初始化这个变量i。
let可以实现块作用域的效果,但是它是ES6语法,在低版本语法的时候如何生成块作用域?
答案是:使用try...catch语句。
for(var i = 0; i < 5; i++) { try { throw(i) } catch(j) { setTimeout(function () { console.log(j); }); } } //打印结果0,1,2,3,4
神奇的效果出现了!
这是因为try...catch语句的catch后面的花括号是一个块作用域,和let的效果一样。所以在try语句块里抛出循环变量i,然后在catch的块作用域里接收到传过来的i,就可以将循环变量保存下来,实现类似闭包和let的效果。