🌞

函数的执行时机

本文将着重探讨函数的执行实机这个问题。

函数的执行时机

函数中变量的值是在执行的时候确定的,而不是在定义的时候。

我们再来看一下这一段代码:

1
2
3
4
5
6
    let i = 0 // 创建一个全局变量 i
    for (i = 0; i < 6; i++) {
      setTimeout(() => {
        console.log(i)
      }, 0)
    }

上面代码运行的结果是将会打印出 6 个 6,这是因为 setTimeout 有 超时延迟 ,如果当前页面(或者操作系统/浏览器本身)被其他任务占用,则会产生延时,等到其他任务执行完之后,setTimeout 里面的函数才会被执行

也就是说,等到该执行 console.log(i) 这句话的时候,i 的值已经被因为循环而修改成了 6

为了让上面的代码能够打印出 0 1 2 3 4 5,我们可以这样改写代码:

1
2
3
4
5
    for (let i = 0; i < 6; i++) {
      setTimeout(() => {
         console.log(i)
      }, 0)
    }

这样 i 的作用域就只在 for 循环 内部,同时每次循环的时候,都会创建一个 新的局部变量 i。当然要注意的是,这并不是改变了函数的执行顺序,console.log(i) 这句话仍然是在 for 循环 执行完毕之后再执行的,我们可以做一个实验:

1
2
3
4
5
6
    for (let i = 0; i < 6; i++) {
      setTimeout(() => {
        console.log(i)
      }, 0)
      console.log('End')
    }

这样写代码最后将会先打印 6 个 'End',再打印 0 1 2 3 4 5

那么还没有别的解决方法呢?

其实是有的,在 let 出现之前,我们仍然有其他的方法来达到想要的效果,下面就简单介绍一下其中的几种方法,他们的核心思路其实都是 把每一次的 i 都保存起来

第一种方法 实际上跟刚才的新语法原理上差不多,只不过更容易理解一些,先上代码:

1
2
3
4
5
6
7
8
    let i   // 创建全局变量 i
    for (i = 0; i < 6; i++) {
      let j = i   // 创建局部变量 j
      setTimeout(() => {
        console.log(j)
      }, 0)
      console.log('End')
    }

原理非常简单,我们在 for 循环 内部创建了一个新的 具有块级作用域的局部变量 j,每次循环的时候,都会创建一个新的 j,于是我们用 j 来保存了每次的 i,当然他也会先打印 6 个 'End',再打印 0 1 2 3 4 5

第二种方法,在 let 诞生之前,我们可以借助函数的传值来保存每一次的 i

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
    var i   // 这里是 let 还是 var 都不要紧,都是创建了一个全局变量,就像最开始的情况一样
    function doSetTimeout(i) {
      setTimeout(() => {
        console.log(i)
      }, 0)
    }
    for (i = 0; i < 6; i++) {
      doSetTimeout(i)
      console.log('End')
    }

看看代码执行的流程:

  1. 首先我们定义了一个中间函数 doSetTimeout
  2. 然后在 for 循环 中调用了这个函数
  3. 每次循环调用的时候,就传入一个参数 idoSetTimeout
  4. doSetTimeout 用形参 i 接收了这个传给他的参数,在这一步,实际上已经产生了 2 个 i
    • 一个是 全局变量 i(也就是 for 循环 中的 i
    • 一个是 doSetTimeout 这个函数里面的 局部变量 i
  5. 每一次循环就会调用 doSetTimeout 一次,同时 doSetTimeout 会给自己创建一个 不一样的、新的局部变量 i
  6. 等循环结束后,doSetTimeout 再打印出所有的 局部变量 i

注意这里仍然没有改变执行的顺序!最终的结果仍然是先打印出 6 个 'End',再打印 0 1 2 3 4 5

当我们理解了上面的写法的时候,我们可以最后看一下在 let 还没有出来的时代,是怎么借助函数创建 块级局部变量 的,也就是我们的 第三种写法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
    var i   // 创建全局变量 i
    for (i = 0; i < 6; i++) {
      (function() {
        var j = i   // 创建局部变量 j
          setTimeout(() => {
            console.log(j)
          }, 0)
      })()
      console.log('End')
    }

这里,我们使用了 立即执行函数(function(...){...})(),这种写法表示我声明一个函数,并且马上执行他。当执行结束之后,因为这个函数没有名字,也没有将地址赋值给某个变量,因此我们没办法再从别的地方再调用他,相当于是“一次性函数”。

var 的作用域是函数级作用域,因此每个函数里面的 j 都是不一样的,我们再一次聪明地用 局部变量 保存了每一次 全局变量 的变化!

总结:这几种写法都是为了利用 局部作用域 ——他们有的利用了 ES 6 为我们提供的 let,有的使用了 函数 ——来保存每一次 for 循环全局变量 不同的值,但是,他们均不会改变执行的顺序!

updatedupdated2020-01-292020-01-29