变量提升是 JavaScript 一个重要的设计缺陷。变量提升这个行为会导致 JavaScript 执行结果的表现与其他的编程语言不太一样,因此初学者往往会对此感到迷惑。虽然 ECMAScript 6 中通过引入块级作用域配合 let 和 const 关键字来避开变量提升的缺陷,但由于 JavaScript 需要保持向下兼容,因此在相当长一段时间内这个缺陷还会继续存在,因此仍有了解的必要。
看一段涉及变量提升的代码:
上面的代码执行结果是 bar
和 undefined
,这个输出结果可能不太好理解。用下面的代码来模拟上面的代码,可以更好地理解 JavaScript 的变量提升:
这段模拟的代码输出的结果与开头给的例子输出的结果一模一样,这应该能明白为什么在定义之前能够使用变量或者函数。
JavaScript 代码执行流程
虽然模拟代码能很好地理解变量提升,但实际上 JavaScript 在执行的过程中并不会发生代码位置变化的情况。为了探究变量提升的真实原因,我们需要了解 JavaScript 代码执行流程。
一段 JavaScript 代码在执行之前需要被 JavaScript 引擎编译,编译完成之后才会进入执行阶段。而变量和函数声明是在编译阶段被 JavaScript 引擎放入内存中的。当一段代码经过编译后,会生成两部分的内容,分别是执行上下文和可执行代码。执行上下文是 JavaScript 执行一段代码时的运行环境,包含了变量、函数以及其他 JavaScript 代码所需的所有信息。
JavaScript 执行流程如下图所示:
变量提升的内容实际上就是在编译阶段存放到变量环境(Variable Environment)对象中的。现在再分析一下文章开头所给出的代码:
- 当遇到
var foo = 'foo'
的时候,会在变量环境对象中创建一个名为foo
的属性,然后使用undefined
作初始化。 - 当遇到
bar
函数的声明的时候,JavaScript 则会在变量环境对象中创建bar
属性,并且该属性的值为指向对中函数的位置。 - 而对于声明之外的代码,则会编译为字节码。字节码中的内容可以类比成模拟代码中的可执行部分。
当编译完毕之后,就有了执行上下文和可执行代码,接下来就到执行阶段:
- 当执行到
bar()
的时候,JavaScript 引擎便开始在变量环境对象中查找该函数,如果变量环境中存在该函数的引用则执行该函数。 - 当执行到
console.log(foo)
的时候,由于变量环境中foo
属性的之为undefined
,因此输出结果为undefined
。 - 执行到
foo = 'foo'
,变量环境中的foo
属性值就变成'foo'
到这里,已经很清楚地了解到变量提升到底是怎么实现的了。
代码编译并创建执行上下文的情况
一段 JavaScript 代码在执行前需要经过编译并创建执行上下文。那什么情况下才会触发编译并创建上下文?一般有三种情况:
- 当 JavaScript 执行全局代码的时候会编译全局代码并创建全局上下文。在整个页面的生命周期内,全局执行上下文只有一份。
- 当调用一个函数的时候,函数体内的代码会被编译并创建函数执行上下文。当执行结束之后,创建的函数执行上下文会被销毁。
- 当使用
eval
函数的时候,eval
的代码也会被编译,并创建执行上下文。