JS函数之执行时机

前言: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
复制代码

因为变量ilet声明的,当前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,比起闭包,会更方便使用,但在方便我们的同时,也会使我们忽略很多细节。但是方案提出来,就是解决问题的,我们不能忘记问题本质,但也要学会使用工具来提升自己的效率,这两方面都是为了提升自己。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享