JS 函数的执行时机
问:以下代码打印出的结果为什么是6个6?
let i = 0 for(i = 0; i<6; i++){ setTimeout(()=>{ console.log(i) },0) }
首先引入概念: setTimeout()定时器
setTimeout() 是属于 window 的方法,该方法用于在指定的毫秒数后调用函数或计算表达式。
语法格式为:
var timeoutID = scope.setTimeout(function[, delay, arg1, arg2, ...]);
var timeoutID = scope.setTimeout(function[, delay]);
var timeoutID = scope.setTimeout(code[, delay]);
// function 是你想要在到期时间 (delay毫秒) 之后执行的函数。
// code 是一个可选语法,你可以使用字符串而不是function ,在delay毫秒之后编译和执行字符串 (使用该语法是不推荐的, 原因和使用 eval()一样,有安全风险)。
// delay 延迟的毫秒数 (一秒等于 1000 毫秒),函数的调用会在该延迟之后发生。如果省略该参数,delay 取默认值 0,意味着“马上”执行,或者尽快执行。
// arg1,arg2,...,argN 是附加参数,一旦定时器到期,它们会作为参数传递给function
setTimeout()函数意思是尽快执行,可以形象地理解为将“手头的事情”做完后执行。具体到文章开头的题目,可以理解为先把 setTimeout() 外的 for 循环执行结束后,再执行 setTimeout() 里面的代码。
自己一开始的疑问:当时认为既然 setTimeout() 是“等一会儿”再执行,那 for 循环结束即“等一会儿”以后, i = 6,此时 console.log(i) 结果应该为 6。为什么会打印出六次呢?
——实际上我错误理解了 “for 循环结束才执行 setTimeout()里面的代码” 的含义。在执行一次 for 循环时,就有一次 console.log(i) 等待执行, for 循环结束以后一共有六次 console.log(i),它们读取到的 i 此时都为 6 ,因此打印 6 次 6。
setTimeout 是一个异步任务,执行到这里的操作会被浏览器丢到另一个任务队列里去, 浏览器这时候会继续执行 for 循环。每一次 for 循环的时候,setTimeout 都执行一次,但是里面的函数没有被执行,而是被放到了任务队列里面。
让上面代码打印 0、1、2、3、4、5 的方法
1. 使用 let 与 for 循环结合
for(let i = 0;i<6;i++){
setTimeout(()=>{ console.log(i) },0)
}
原理:
- 此时 i 只会在 for 循环的作用域里面执行,let 只能声明一次且只在本轮循环中生效。每次循环的 i 都是一个新的变量,setTimeout 里输出的 i 就是这个新的 i。一次你打印的结果为:0、1、2、3、4、5。
2. 使用立即执行函数
let i = 0
for(i = 0; i<6; i++){
! function(j){
setTimeout(()=>{
console.log(j)
},0)}(i)
}
原理:
- 声明匿名函数 function(value){} 包裹 setTimeout();
- 再在匿名函数后加个 () 立即调用,并把 i 当作参数 value 传入匿名函数进行调用;
- 由于 JS 认为在匿名函数后加个 () 立即调用的语法不合法,JS 程序员发现只需在匿名函数前加个运算符即可,通常用 !。
3. 使用 setTimeout 的第三个参数
查看 MDN 可知 setTimeout 有附加参数,附加参数会作为参数传递给 function
var timeoutID = scope.setTimeout(function[, delay, arg1, arg2, ...]);
// arg1,arg2,...,argN 是附加参数,一旦定时器到期,它们会作为参数传递给function
因此可以使用:
let i = 0
for(i = 0; i<6; i++){
setTimeout((j)=>{
console.log(j)
},0,i)// i 作为附加参数,传给 setTimeout 里面的箭头函数
}