前言:ES 6 新增了
let
命令,用来声明变量,不同于var
,使用let
声明的变量仅在块级作用域内有效,所以for
循环的计数器,就很适合let
命令。
两段代码
先看下面这个例子:
代码1
let i = 0
for(i = 0; i<6; i++){
setTimeout(()=>{
console.log(i)
},0)
}//返回6个6
复制代码
再看这个例子:
代码2
for(let i = 0; i<6; i++){
setTimeout(()=>{
console.log(i)
},0)
}//依次打印出0 1 2 3 4 5
复制代码
为什么两次会打印出不同的结果呢?
关于setTimeout()
setTimeout()
方法用于在指定的毫秒数后调用函数或执行表达式。返回一个 ID(数字),可以将这个ID传递给clearTimeout()
来取消执行。
因为JS是单线程的,单线程就意味着所有任务需要排队,前一个任务结束后,才会执行后一个任务。当代码执行遇到setTimeout(() => {},millisec)
时,会把函数放在任务队列中,当JS引擎线程空闲时并达到指定时间时,才会把函数放到JS引擎线程中执行。
当setTimeout()
的时间参数为0时,也必须等到当前代码(执行栈)执行完,主线程才会去执行它指定的回调函数。
原理
代码1很好理解,为什么会返回6个6。
let i = 0
for(i = 0; i<6; i++){
setTimeout(()=>{
console.log(i)
},0)
}//返回6个6
复制代码
当前在执行for循环,现在有一个setTimeout()
,且时间参数为0,那么当执行完当前for循环后,就会去执行这个setTimeout()
,即打印出i。也就是说,执行完for循环后,有6个i=6
在等待执行console.log(i)
,故而打印出6个6。
代码2会有些难理解,源于let
的特性。
for(let i = 0; i<6; i++){
setTimeout(()=>{
console.log(i)
},0)
}//打印出0 1 2 3 4 5
复制代码
因为变量i
是let
声明的,当前i
只在本轮循环有效,所以每一次循环的i
其实都是一个新变量。虽然每一轮循环i
都会重新声明,但是JS内部引擎会记住上一轮循环的值,初始化上一轮变量i
的同时,保证了下一轮循环可以在上一轮循环的基础上进行计算,每次循环的i
其实都是一个新的变量。
这样,6次循环依次得出0/1/2/3/4/5,等待for循环结束后,setTimeout()
开始执行,打印出0 1 2 3 4 5。
还有其他实现代码2的方法吗?
立即执行函数
从网上看到了一种方法,在ES6出来之前,是可以通过闭包的方式去实现这一操作的。
在这之前先看一段代码。
for(var i = 0; i < 6; i++){
setTimeout(function(){
console.log(i);
},0);
}//打印出6个6
复制代码
当用var
命令声明变量i
时,其实是在全局范围内有效的,所以全局只有一个变量i
,同理于代码1,会打印出6个6。
让我们把思路颠倒,原来在用let
时,追问为什么会打印出0 1 2 3 4 5(JS“优化”let,让我们更方便使用);而现在,当我们倒回没有ES6的时代,怎样实现打印0 1 2 3 4 5呢?
就是刚才说到的使用闭包方法。
for( var i = 0;i<6;i++) {
!function(i){
setTimeout(function () {
console.log(i);
},0)
}(i);
}//打印出0 1 2 3 4 5
复制代码
之所以能够实现,是因为闭包里setTimeout()
并不是暴露在全局环境下,而是在闭包的独立作用域中。
在匿名函数前加运算符!、~、()、+、-,就叫做立即执行函数。在ES 5 时代,为了得到局部变量,必须引入一个函数。但是这个函数如果有名字,就得不偿失。于是这个函数必须是匿名函数,声明匿名函数,然后立即加个 () 执行它。但是 JS 标准认为这种语法不合法。
心得
ES6提供的let
,比起闭包,会更方便使用,但在方便我们的同时,也会使我们忽略很多细节。但是方案提出来,就是解决问题的,我们不能忘记问题本质,但也要学会使用工具来提升自己的效率,这两方面都是为了提升自己。