本文将着重探讨函数的执行实机这个问题。
函数的执行时机
函数中变量的值是在执行的时候确定的,而不是在定义的时候。
我们再来看一下这一段代码:
|
|
上面代码运行的结果是将会打印出 6 个 6
,这是因为 setTimeout 有 超时延迟 ,如果当前页面(或者操作系统/浏览器本身)被其他任务占用,则会产生延时,等到其他任务执行完之后,setTimeout 里面的函数才会被执行。
也就是说,等到该执行 console.log(i)
这句话的时候,i
的值已经被因为循环而修改成了 6
。
为了让上面的代码能够打印出 0 1 2 3 4 5
,我们可以这样改写代码:
|
|
这样 i
的作用域就只在 for 循环
内部,同时每次循环的时候,都会创建一个 新的局部变量 i
。当然要注意的是,这并不是改变了函数的执行顺序,console.log(i)
这句话仍然是在 for 循环
执行完毕之后再执行的,我们可以做一个实验:
|
|
这样写代码最后将会先打印 6 个 'End'
,再打印 0 1 2 3 4 5
。
那么还没有别的解决方法呢?
其实是有的,在 let
出现之前,我们仍然有其他的方法来达到想要的效果,下面就简单介绍一下其中的几种方法,他们的核心思路其实都是 把每一次的 i
都保存起来:
第一种方法 实际上跟刚才的新语法原理上差不多,只不过更容易理解一些,先上代码:
|
|
原理非常简单,我们在 for 循环
内部创建了一个新的 具有块级作用域的局部变量 j
,每次循环的时候,都会创建一个新的 j
,于是我们用 j
来保存了每次的 i
,当然他也会先打印 6 个 'End'
,再打印 0 1 2 3 4 5
。
第二种方法,在 let
诞生之前,我们可以借助函数的传值来保存每一次的 i
:
|
|
看看代码执行的流程:
- 首先我们定义了一个中间函数
doSetTimeout
- 然后在
for 循环
中调用了这个函数 - 每次循环调用的时候,就传入一个参数
i
给doSetTimeout
, doSetTimeout
用形参i
接收了这个传给他的参数,在这一步,实际上已经产生了 2 个i
:- 一个是 全局变量
i
(也就是for 循环
中的i
) - 一个是
doSetTimeout
这个函数里面的 局部变量i
- 一个是 全局变量
- 每一次循环就会调用
doSetTimeout
一次,同时doSetTimeout
会给自己创建一个 不一样的、新的局部变量i
- 等循环结束后,
doSetTimeout
再打印出所有的 局部变量i
注意这里仍然没有改变执行的顺序!最终的结果仍然是先打印出 6 个
'End'
,再打印0 1 2 3 4 5
。
当我们理解了上面的写法的时候,我们可以最后看一下在 let
还没有出来的时代,是怎么借助函数创建 块级局部变量 的,也就是我们的 第三种写法:
|
|
这里,我们使用了 立即执行函数:(function(...){...})()
,这种写法表示我声明一个函数,并且马上执行他。当执行结束之后,因为这个函数没有名字,也没有将地址赋值给某个变量,因此我们没办法再从别的地方再调用他,相当于是“一次性函数”。
var
的作用域是函数级作用域,因此每个函数里面的 j
都是不一样的,我们再一次聪明地用 局部变量 保存了每一次 全局变量 的变化!
总结:这几种写法都是为了利用 局部作用域 ——他们有的利用了 ES 6 为我们提供的
let
,有的使用了 函数 ——来保存每一次for 循环
中 全局变量 不同的值,但是,他们均不会改变执行的顺序!