变量提升:JavaScript代码是按顺序执行的吗?

73 阅读5分钟

JavaScrip 的执行上下文,你才能更好地 理解 JavaScript 语言本身,比如变量提升、作用域和闭包等。不仅如此,理解执行上下文 和调用栈的概念还能助你成为一名更合格的前端开发者。

所谓的变量提升,是指在 JavaScript 代码执行过程中,JavaScript 引擎把变量的声明部分和函数的声明部分提升到代码开头的“行为”。变量被提升后,会给变量设置默认值,这个默认值就是我们熟悉的 undefined。

image.png

实际上变量和函数声明在代码里的位置是不会改变的,而且是在编译阶段被 JavaScript 引擎放入内存中。一段 JavaScript 代码在执行之前需要被 JavaScript 引擎编译,编译完成之后,才会进入执行阶段。

image.png

image.png

执行上下文 (Execution context)和可执行代码。执行上下文是 JavaScript 执行一段代码时的运行环境,在执行上下文中存在一个变量环境的对象 (Viriable Environment),该对象中保存了变量提升的内容,比如上面代码中的变量 myname 和函数 showName,都保存在该对象中。

变量环境对象看成是如下结构:
1 VariableEnvironment:
2  myname -> undefined,
3  showName ->function : {console.log(myname)

如何生成变量环境对象的?

1 showName()
2 console.log(myname)
3 var myname = '极客时间'
4 function showName() {
5    console.log('函数 showName 被执行');
6 }

第 1 行和第 2 行,由于这两行代码不是声明操作,所以 JavaScript 引擎不会做任何处理;第 3 行,由于这行是经过 var 声明的,因此 JavaScript 引擎将在环境对象中创建一个名为 myname 的属性,并使用 undefined 对其初始化;第 4 行,JavaScript 引擎发现了一个通过 function 定义的函数,所以它将函数定义存储到堆 (HEAP)中,并在环境对象中创建一个 showName 的属性,然后将该属性值指向堆中函数的位置,这样就生成了变量环境对象。接下来 JavaScript 引擎会把声明以外的代码编译为字节码。

1 showName()
2 console.log(myname)
3 myname = '极客时间'

有了执行上下文和可执行代码那么接下来就到了执行阶段。

当执行到 showName 函数时,JavaScript 引擎便开始在变量环境对象中查找该函数,由于变量环境对象中存在该函数的引用,所以 JavaScript 引擎便开始执行该函数,并输出“函数 showName 被执行”结果。接下来打印“myname”信息,JavaScript 引擎继续在变量环境对象中查找该对象,由 于变量环境存在 myname 变量,并且其值为 undefined,所以这时候就输出 undefined。接下来执行第 3 行,把“极客时间”赋给 myname 变量,赋值后变量环境中的 myname 属性值改变为“极客时间”,变量环境如下所示:

1 VariableEnvironment: 
2 myname -> " 极客时间 ", 
3 showName ->function : {console.log(myname)

以上就是一段代码的编译和执行流程。实际上,编译阶段和执行阶段都是非常复杂 的,包括了词法分析、语法解析、代码优化、代码生成等。

代码中出现相同的变量或者函数怎么办?

在执行一段 JavaScript 代码之前,会编译代码,并将代码中的函数和变量保存到执行上下文的变量环境中,那么如果代码中出现了重名的函数或者变量,JavaScript 引擎会如何处理?

1 function showName() {  
2 console.log('极客邦'); 3}  
4 showName();  
5 function showName() {  
6 console.log('极客时间'); 7}  
7 showName();

完整执行流程:首先是编译阶段。遇到了第一个 showName 函数,会将该函数体存放到变量环境中。接 下来是第二个 showName 函数,继续存放至变量环境中,但是变量环境中已经存在一个 showName 函数了,此时,第二个 showName 函数会将第一个 showName 函数覆盖掉。这样变量环境中就只存在第二个 showName 函数了。接下来是执行阶段。先执行第一个 showName 函数,但由于是从变量环境中查找 showName 函数,而变量环境中只保存了第二个 showName 函数,所以最终调用的是第二个函数,打印的内容是“极客时间”。第二次执行 showName 函数也是走同样的流程,所以输出的结果也是“极客时间”。综上所述,一段代码如果定义了两个相同名字的函数,那么最终生效的是最后一个函数

总结: JavaScript 代码执行过程中,需要先做变量提升,而之所以需要实现变量提升,是因为JavaScript 代码在执行之前需要先编译。

在编译阶段,变量和函数会被存放到变量环境中,变量的默认值会被设置为 undefined;在代码执行阶段,JavaScript 引擎会从变量环境中去查找自定义的变量和函数。

如果在编译阶段,存在两个相同的函数,那么最终存放在变量环境中的是最后定义的那个,这是因为后定义的会覆盖掉之前定义的。

JavaScript 的执行机制:先编译,再执行

函数提升要比变量提升的优先级要高一些,且不会被变量声明覆盖,但是会被变量赋值之后覆盖。