众所周知,JavaScript 代码并不是按照从上到下按照顺序执行,存在变量提升和函数提升。我们先来看下什么是变量提升和函数提升。
# 变量提升
通常 JavaScript 引擎在正式执行之前先进行一次预编译,在这个过程中,首先将变量声明及函数声明提升到当前作用域的顶端,然后进行接下来的处理。
console.log(a); // undefined
var a = 1;
2
运行代码,会发现打印的结果是 undefined
,在变量还没有出现之前就调用,为什么不会出错呢?这是因为 JS 经过一次预编译之后,上面的代码等价于:
var a;
console.log(a);
a = 1;
2
3
a 只有声明的情况下初始值就是 undefined。所以不会出错而打印 undefined。
# 函数提升
同样的,JavaScript 引擎也会把函数声明提升到当前作用域的顶部。举个例子:
hoisted() // I am hoisted
function hoisted() {
console.log('I am hoisted')
}
2
3
4
综上,JavaScript 引擎并不是一行一行地分析和执行程序,而是一段一段地分析执行。当执行一段代码的时候,会进行一个“准备工作”,那么这个“一段一段”中的“段”是怎么划分的?“准备工作“又是什么?
# 可执行代码
这就要说到 JavaScript 的可以执行代码有那些了?
总共有三种类型:全局代码、函数代码、eval代码。
当 JS 引擎执行到一个函数时,就会进行”准备工作“,这里的”准备工作“专业一点的说法,就叫做”执行上下文(execution context)“。
简而言之,执行上下文就是当前 JavaScript 的代码被解析和执行时所在环境的抽象概念,JavaScript 中运行任何的代码都是在执行上下文中运行。
# 执行上下文栈
为了存储和管理创建的执行上下文,JavaScript 引擎创建了执行上下文栈(Execution context stack, ECS)。
我们定义执行上下文栈为一个数组来模拟执行上下文栈的行为。
ECStack = [];
当 JavaScript 开始解释执行代码的时候,最先遇到的就是全局代码,所以初始化的时候首先向执行上下文压入一个全局执行上下文,用 globalContext 表示它,并且只有当整个应用程序结束的时候,ECStack 才会清空,所以程序在结束之前,ECStack 底部永远有一个 globalContext。
ECStack = {
globalContext
}
2
3
假设 JavaScript 遇到下面的一段代码
function func3() {
console.log('func3')
}
function func2() {
console.log('func2')
func3()
}
function func1() {
console.log('func1')
func2()
}
func1()
2
3
4
5
6
7
8
9
10
11
12
当执行一个函数时就会创建一个执行上下文,并且压入执行上下文栈,当函数执行完毕之后,则会将执行上下文从执行上下文栈弹出。知道工作原理之后,我们看看 JavaScript 引擎是怎么处理上面这段代码:
// func1()
ECStack.push(<func1> functionContext);
// func1中还调用了func2,创建一个func2的执行上下文
ECStack.push(<fun2> functionContext);
// func2调用了func3,创建一个func3的执行
ECStack.push(<fun3> functionContext);
// func3执行完毕
ECStack.pop()
// func2执行完毕
ECStack.pop()
// func1执行完毕
ECStack.pop()
// ...继续执行接下来的代码
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19