javasctipt的执行过程
1. 初始化全局对象
首先,JS引擎会在执行代码之前,也就是解析代码时,会在我们的堆内存创建一个全局对象:Global Object(简称GO),观察以下代码,在全局中定义了几个变量:
var name = 'curry'
var message = 'I am a coder'
var num = 30
JS引擎内部在解析以上代码时,会创建一个全局对象(伪代码如下):
(1). 全局访问 所有的**作用域(scope)**都可以访问该全局对象; (2). 包含全局方法和类,还包含有一个指向该全局对象自身的window属性 对象里面会包含一些全局的方法和类,像Math、Date、String、Array、setTimeout等等; 其中有一个window属性是指向该全局对象自身的; (3). 收集全局变量 该对象中会收集我们上面全局定义的变量,并设置成undefined; 全局对象是非常重要的,我们平时之所以能够使用这些全局方法和类,都是在这个全局对象中获取的;
var GlobalObject = {
Math: '类',
Date: '类',
String: '类',
setTimeout: '函数',
setInterval: '函数',
window: GlobalObject,
...
name: undefined,
message: undefined,
num: undefined
}
2. 执行上下文栈(调用栈)
JS引擎为了执行代码,引擎内部会有一个执行上下文栈(Execution Context Stack,简称ECS),它是用来执行代码的调用栈。
(1)ECS如何执行?先执行谁呢?
- 无疑是先执行我们的全局代码块;
- 在执行前全局代码会构建一个全局执行上下文(Global Execution Context,简称GEC);
- 一开始【全局执行上下文】就会被放入到【调用栈】ECS中执行
(2)全局执行上下文(GEC)包含那些内容呢? **第一部分:**执行代码前。 在转成抽象语法树之前,会将全局定义的变量、函数等加入到Global Object中,也就是上面初始化全局对象的过程; 但是并不会真正赋值(表现为undefined),所以这个过程也称之为变量的作用域提升(hoisting); **第二部分:**代码执行。 对变量进行赋值,或者执行其它函数等;
下面就通过一幅图,来看看GEC被放入ECS后的表现形式:
3. 调用栈调用GEC的过程
来看看调用栈调用全局执行上下文(GEC)的过程。 实例代码:
var name = 'curry'
console.log(message)
var message = 'I am a coder'
function foo() {
var name = 'foo'
console.log(name)
}
var num1 = 30
var num2 = 20
var result = num1 + num2
foo()
调用栈调用过程:
- 初始化全局对象
- 这里需要注意的是函数存放的是地址,会指向函数对象,与普通变量有所不同;
- 从上往下解析JS代码,当解析到foo函数时,因为foo不是普通变量,并不会赋为undefined,JS引擎会在堆内存中开辟一块空间存放foo函数,在全局对象中引用其地址;
- 这个开辟的函数存储空间最主要存放了该函数的父级作用域和函数的执行体代码块;
- 构建一个全局执行上下文(GEC),代码执行前将VO的内存地址指向GlobalObject(GO)。
- 将全局执行上下文(GEC)放入执行上下文栈(ECS)中
- 从上往下开始执行全局代码,依次对GO对象中的全局变量进行赋值
- 当执行var name = 'curry'时,就从VO(对应的就是GO)中找到name属性赋值为curry;
- 接下来执行console.log(message),就从VO中找到message,注意此时的message还为undefined,因为message真正赋值在下一行代码,所以就直接打印undefined(也就是我们经常说的变量作用域提升);
- 后面就依次进行赋值,执行到var result = num1 + num2,也是从VO中找到num1和num2两个属性的值进行相加,然后赋值给result,result最终就为50;
- 最后执行到foo(),也就是需要去执行foo函数了,这里的操作是比较特殊的,涉及到函数执行上下文,下面来详细了解;
4. 函数执行上下文
在执行全局代码遇到函数如何执行呢?
- 在执行的过程中遇到函数,就会根据函数体创建一个函数执行上下文(Functional Execution Context,简称FEC),并且加入到执行上下文栈(ECS)中。
- 函数执行上下文(FEC)包含三部分内容:
- AO:在解析函数时,会创建一个Activation Objec(AO);
- 作用域链:由函数VO和父级VO组成,查找是一层层往外层查找;
- this指向:this绑定的值,在函数执行时确定;
- 其实全局执行上下文(GEC)也有自己的作用域链和this指向,只是它对应的作用域链就是自己本身,而this指向为window。
继续来看上面的代码执行,当执行到foo()时:
- 先找到foo函数的存储地址,然后解析foo函数,生成函数的AO;
- 根据AO生成函数执行上下文(FEC),并将其放入执行上下文栈(ECS)中;
- 开始执行foo函数内代码,依次找到AO中的属性并赋值,当执行console.log(name)时,就会去foo的VO(对应的就是foo函数的AO)中找到name属性值并打印;
5. 变量环境和记录
上文中提到了很多次VO,那么VO到底是什么呢?下面从ECMA新旧版本规范中来谈谈VO。
在早期ECMA的版本规范中:每一个执行上下文会被关联到一个变量环境(Variable Object,简称VO),在源代码中的变量和函数声明会被作为属性添加到VO中。对应函数来说,参数也会被添加到VO中。
- 也就是上面所创建的GO或者AO都会被关联到变量环境(VO)上,可以通过VO查找到需要的属性;
- 规定了VO为Object类型,上文所提到的GO和AO都是Object类型;
在最新ECMA的版本规范中:每一个执行上下文会关联到一个变量环境(Variable Environment,简称VE),在执行代码中变量和函数的声明会作为**环境记录(Environment Record)**添加到变量环境中。对于函数来说,参数也会被作为环境记录添加到变量环境中。
- 也就是相比于早期的版本规范,对于变量环境,已经去除了VO这个概念,提出了一个新的概念VE;
- 没有规定VE必须为Object,不同的JS引擎可以使用不同的类型,作为一条环境记录添加进去即可;
- 虽然新版本规范将变量环境改成了VE,但是JavaScript的执行过程还是不变的,只是关联的变量环境不同,将VE看成VO即可;
6.全局代码执行过程(函数嵌套)省略。。。
总结:
- 函数在执行前就已经确定了其父级作用域,与函数在哪执行没有关系,以函数声明的位置为主;
- 执行代码查找变量属性时,会沿着作用域链一层层往上查找(沿着VO往上找),如果一直找到全局对象中还没有该变量属性,就会报错未定义;
- 上文中提到了很多概念名词,下面来总结一下:
| 名词 | 解释 |
|---|---|
| ECS | 执行上下文栈(Execution Context Stack),也可称为调用栈,以栈的形式调用创建的执行上下文 |
| GEC | 全局执行上下文(Global Execution Context),在执行全局代码前创建 |
| FEC | 函数执行上下文(Functional Execution Context),在执行函数前创建 |
| VO | Variable Object,早期ECMA规范中的变量环境,对应Object |
| VE | Variable Environment,最新ECMA规范中的变量环境,对应环境记录 |
| GO | 全局对象(Global Object),解析全局代码时创建,GEC中关联的VO就是GO |
| AO | 函数对象(Activation Object),解析函数体代码时创建,FEC中关联的VO就是AO |