阅读 37

JS函数的执行时机

前言

JS函数在执行的时候,函数所处的位置不同,执行的结果就会各不相同。
下面我们通过几个例子来看一下,函数的执行时机对结果会产生怎样的影响。

正文

- 例1

let a = 1
function fn(){
	console.log(a)
}
复制代码

Q: 请问上例会打印出多少?
A:不知道,因为你压根没有调用函数fn。

- 例2

let a = 1
function fn(){
	console.log(a)
}
fn()
复制代码

Q: 请问上例会打印出多少?
A:很显然,是1。

- 例3

let a = 1
function fn(){
	console.log(a)
}
a = 2
fn()
复制代码

Q: 请问上例会打印出多少?
A:因为fn在a=2后执行,故打印出2。

- 例4

let a = 1
function fn(){
	console.log(a)
}
fn()
a = 2
复制代码

Q: 请问上例会打印出多少?
A:对比一下例3我们可以知道,fn先调用的,故会打印出1。

前面几个例子都比较简单,相信大家都可以回答出来。那么我们现在升级一下难度,请看下面这个例子。

- 例5

Q: 请问下例的执行结果是什么?

let a = 1
function fn(){
  setTimeout(()=>{
  	console.log(a)
  },0)
}
fn()
a = 2
复制代码

有的小白同学可能会觉得,这个应该和例4差不多吧,fn在a=2前调用应该也是打印出1。
实际上,本例会打印出2。

我们来分析一下。

这里fn使用了一个定时器setTimeout,设置的延迟时间是0,这里涉及到“零延迟” ,我们可以理解为“尽快打印出a”。

JS 是单线程的,且setTimeout是一个异步任务,当定时器事件被触发时,该线程会把事件添加到任务队列的队尾,等待 JS 引擎的处理。

上面的解释没有看懂?没关系,我来进一步解释一下,何为“零延迟”。

零延迟 (Zero delay)并不是意味着回调会立即执行。

在零延迟调用 setTimeout 时,并不是过了给定的时间间隔后就马上执行回调函数。> 其等待的时间基于队列里正在等待的消息数量。

也就是说,setTimeout()只是将事件插入了任务队列,必须等到当前代码执行完,主线程才会去执行它指定的回调函数。> 要是当前代码耗时很长,有可能要等很久,所以并没有办法保证回调函数一定会在setTimeout()指定的时间执行。

现在我们知道为什么会打印出2。因为只有在执行完主线程的所有代码之后,主线程空了,才会去任务队列中取任务,去执行回调函数。

理解了上面这部分内容,我们再看下一个例子。

- 例6

Q: 请问下例的执行结果是什么?

let i = 0
for(i = 0; i<6; i++){
  setTimeout(()=>{
  	console.log(i)
  },0)
}
复制代码

如果大家对上面这个例子能够理解,那么这题一定也难不倒大家。
结果是会打印出“6个6”,而不是0,1,2,3,4,5。

我们再来分析一下。

  • 这里主线程上面要执行的任务是:for循环。由于每执行一次for循环就要设一个定时,就好比是每次循环“定一个闹钟”。一共执行6次循环,那么就定了6个闹钟。
  • 在上例中,我们解释了,在零延迟调用setTimeout时,所有的定时任务被放在了任务队列的队尾,只有主线程的任务全部完成,才会去任务队列执行这6个定时任务。
  • 又,主线程结束时i=6。故,最终会打印“6个6”。

好了,我们现在可以理解上述的运行机制了,那么请问,有什么办法可以打印出0,1,2,3,4,5?
请看例7。

- 例7

for(let i = 0; i<6; i++){
  setTimeout(()=>{
  	console.log(i)
  },0)
}
复制代码

将let写到for循环里面,就可以打印出0,1,2,3,4,5。是不是很神奇,不信你可以试试看~😛😛😛


那么,为什么呢。。。

这里就涉及到ES6中let的一些机制了。。。

你可以这么理解,上面的这个例子等同于下面:

for(let i = 0; i<6; i++){
  let j = i
  setTimeout(()=>{
  	console.log(j)
  },0)
}
复制代码

具体来说,就是for( let i = 0; i< 6; i++) { 循环体 } 在每次执行循环体之前,JS 引擎会把 i 在循环体的上下文中重新声明及初始化一次。
关于let的一些细节,推荐大家阅读一下方应杭老师写的我用了两个月的时间才理解let

拓展

我们再对上面这个例子做一个拓展。
请大家思考一下,还有没有别的方法打印出0,1,2,3,4,5?
我总结了几个例子。

  • 使用中间变量
let i
for( i = 0; i<6; i++){
  let j = i
  setTimeout(()=>{
  	console.log(j)
  },0)
}
复制代码

在每次循环的时候用 let j 保留的 i 的值,所以在 i 变化的时候,j 并不会变化。而console.log 的是 j,所以不会出现 6 个 6。

  • 使用闭包
let i 
for(i = 0; i<6; i++){
  !function(j){
      setTimeout(()=>{
        console.log(j)
      },0)
  }(i)
}
复制代码
  • 设置setTimeout第三个参数为i
let i
for(i = 0; i<6; i++){
    setTimeout((value)=>{
      console.log(value)
    },0,i)
}
复制代码
  • 使用forEach()
let arr=[0,1,2,3,4,5]
arr.forEach.call(arr,item => console.log(item))
复制代码
  • 使用for in
let arr1 = [0,1,2,3,4,5]
for(i in arr1){
    console.log(i)
}
复制代码

后记

本文参考的相关文章:

由于本人水平有限,如有描述不准确的地方请给我留言,欢迎交流~
本文为个人原创,转载请注明出处。