JavaScript代码的执行顺序

今天我们来研究一下Javascript的执行顺序,了解它是怎么如何运行的。
首先我们来看一段代码。

1
2
3
4
5
6
showName();
console.log('name', name);
var name = 'able';
function showName() {
console.log('执行函数showName');
}

Javascript是按顺序执行的,那么看上去在执行到第一行的时候函数showName并没有定义,那么它是不是会报错呢。想必大家应该都知道,它的结果其实是执行函数showName;name undefined;
那么通过上边的执行结果,我们可以认识到,函数或者变量可以在它被定义之前使用。对于变量而言,如果它在执行过程中未声明,那么js执行会报错。如果在一个变量定义之前使用它,不会报错,但是该变量的值是undefined。如果在一个函数定义之前执行它,不会报错,而且函数能正常执行。
那可能就会产生一些疑问了,为啥子变量和函数能在定义之前使用呢,js不是想象中一行一行执行的么。都是提前使用,为何函数能正常执行,变量值却是undefined。我们来一步一步分析。
首先我们了解一下js中的声明和赋值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//变量
var name = 'able';
//这段代码可以分为两部分
var name //声明部分
name = 'able'; //执行部分

//函数
function showName() {
console.log('hahhahaa');
}//完整的函数声明,不涉及到赋值

var showName = function() {
console.log('wowowow');
}
var showName //声明部分
showName = function() {
console.log('wowowow');
} //赋值部分

了解声明和赋值之后,,我们先来聊聊变量提升。
变量提升呢,它是指在Javascript代码的执行过程中,JS引擎把变量的声明部分和函数的声明部分提升到代码开头的‘行为’。变量被提升后,会给变量设置默认值‘undefined’。下面我们用代码来模拟一下变量提升。

1
2
3
4
5
6
7
8
9
10
//变量提升部分
var name = undefined;
function showName() {
console.log('showName被调用‘);
}

//可执行代码部分
showName();
console.log('name', name);
name = 'able';

可以发现,它的执行结果和我们最开始的代码是一毛一样的。那么我们也就理解了为啥子可以在定义之前使用函数或者变量的原因了——函数和变量在执行之前都提升到了代码开头。
从字面上来看,变量提升意味着变量和函数的声明会移动到代码的开头,就像我们上边模拟的那样,但事实上,这可能不太准确,变量和函数声明在代码中的位置是不会被改变的,而是在编译阶段会被JS引擎放入到内存中。这里又涉及到JS的执行流程了,一段JS代码在执行之前需要被引擎编译,编译完成后才会进入执行阶段。
一段代码在经过编译后,会生成两部分内容:执行上下文和可执行代码。所谓执行上下文呢,它是指JS执行一段代码时的运行环境。比如调用一个函数,就会进入这个函数的执行上下文,确定该函数在执行期间用到的this、变量、对象以及函数等。在执行上下文中存在一个变量环境的对象,它里边保存了变量提升的内容,最开始我们研究的那段代码中,变量name和函数showName,都保存在该对象中。我们结合代码来分析一下如何生成变量环境对象。

1
2
3
4
5
6
showName();
console.log('name', name);
var name = 'ableF';
function showName() {
console.log('安排name‘);
}

第一行第二行不是声明操作,所以引擎不会做任何处理。第三行有var声明,因此引擎将在环境对象中创建一个名为name的属性,并使用undefined对其初始化。第四行,引擎发现了一个通过function定义的函数,它将函数存储到堆中,并在环境变量中创建了一个showName的属性,然后将该属性值指向堆中函数的位置,这样就生成了变量环境对象。
我们可以用一张图来形象滴展示Js的执行流程。
Js执行机制.jpg
那么在有了执行上下文和可执行代码后,我们就可以进入执行阶段了。当执行showName函数时,JS引擎便在变量环境对象中查找该函数,由于变量环境对象中存在该函数的引用,所以JS引擎便开始执行该函数,并且输出‘执行函数showName‘的结果;接下来打印name信息,JS引擎在变量环境对象中查找该对象,由于变量环境中存在name变量且值为undefined,因此输出undefined。接下来把‘able’赋值给name变量,赋值后变量环境中的name属性值改为‘able’。
那么如果在代码中出现相同的变量或者函数会发生什么呢,我们来看下面一段代码。

1
2
3
4
5
6
7
8
function geName() {
console.log('able');
}
getName();
function getName() {
console.log('moriaty');
}
getName();

我们来分析一下它的完整执行流程。首先是编译阶段,遇到了第一个getName函数,会把该函数放到变量环境中。接下来是第二个getName函数,继续存放到变量环境中,但是变量环境中已经有一个getName了,此时第二个会将第一个覆盖。那么可想而知,在执行阶段的时候,两个getName函数的执行都调用的是第二个函数。
总结
JavaScript的执行机制,先编译,再执行。在编译阶段,变量和函数会被存放到变量环境中,变量的默认值会被设置为undefined;在代码执行阶段,JavaScript引擎会从变量环境中去查找自定义的变量和函数。并且如果在编译阶段如果存在两个相同的函数,那么最终存放在变量环境中的是最后定义的那个,因为它的逻辑是后定义的会覆盖先定义的。