上文的结尾我们提到了js的执行过程,那么下面我们就来谈一下js的执行过程
一、代码被解析——初始化全局对象
VO(Variable Object):全局变量对象
AO(Active Object):私有变量对象
JavaScript源代码在通过解析(Parse)转为AST之前,v8引擎会在堆内存中创建一个全局对象GlobalObject(GO)
- 这个对象所有的作用域都可以访问
- 包括Data,String,Number,console.log等都会放在GO中
- 其中还有一个属性:window指向自己(GO)
- 全局的变量和函数都会放在GO中,不同的是:在编译阶段,变量的值是undefined,而函数的值是 函数体在内存中的地址
二、运行代码
1.ECS
v8为了执行代码,v8引擎内部会有一个执行上下文栈(Execution Context Stack,ECS),也叫做函数调用栈,它用于执行代码中的调用栈。
2.GEC
ECS要执行什么呢?执行的是全局的代码块
- 为了执行全局的代码块,会创建一个全局执行上下文Global Execution Context(GEC)
- 在GEC中,VO指的就是GO
- GEC会被放在ECS中执行
3.GEC被放入ECS中执行,其中有两个过程:
- 在代码执行前,在parser转成AST的过程中,会将全局定义的变量,函数等加入到GO中(在上面提到过),如果是变量,那么仅仅是对变量做声明,并不会对它们进行赋值,这个过程也叫做变量的作用域提升;如果是函数,那么除了存放函数名,还会存放函数的值,也就是函数体在内存中的位置
- 在代码执行过程中,代码顺序执行,比如对变量赋值,或者执行其他函数
4.FEC
ECS要执行什么呢?执行的是全局的代码块
为了执行全局的代码块,会创建一个全局执行上下文(GEC)
如果在执行过程中遇到函数,就会根据函数体创建一个函数执行上下文(Functional Execution Context,FEC),并且压入ECS栈中
在FEC中,VO指的是AO(Active Object):用于存放函数体内部的变量,函数。同样,在编译阶段,变量的值是undefined,函数的值是 函数体在内存中的地址
-
FEC中包含三部分内容:
1)在解析函数称为AST树时,会创建AO,AO中包含形参、arguments、函数定义和指向函数对象、定义的变量
2)作用域链(scope chain):由VO(在函数中就是AO)和父级VO(父级如果是全局对象,那么就是GO,如果是私有(函数)对象,那么就是AO)组成,查找时由内向外一层层查找
3)this绑定的值,这个后面会详细叙述
三、案例1
var name = 'captain'
console.log(name)
console.log(num1)
var num1 = 20
var num2 = 30
var sum = num1 + num2
console.log(sum)
在上述代码中,
1.创建全局对象GO
- 在编译阶段,除了包括内置的变量、类、方法等之外,还会将上述代码中的name、num1,num2,sum等名称加入到GO中,相当于在全局做了变量声明,只知道变量存在,但不知道具体的值
2.然后执行代码
把GEC放入到ECS中,GEC中的VO就是GO,然后顺序执行代码,执行代码过程中,遇到变量、函数,会去GEC中的VO中找,也就是GO中找
- 第一行,对变量name进行赋值
- 第二行,打印name的值为captain
- 第三行,打印num1的值为undefined,这是因为目前只做了变量的声明,但是还没有赋值,所以不会报错说没有这个变量,也不会说变量的值,只会打印undefined
- 第四行,对num1进行赋值
- 第五行,对num2进行赋值
- 第六行,对sum进行赋值
- 第七行,打印sum的值为50
四、案例2
var name = 'captain'
foo()
function foo() {
var num = 20
console.log(name)
}
foo()
在上述代码中
1.创建全局对象GO和私有对象AO
- 在编译阶段,除了包括内置的变量、类、方法等之外,还会将name存入GO中,name的值为undefined,还有foo也会放入GO中,foo的值为foo函数体的内存地址
- 对函数foo函数体进行预编译,将num存放在AO中,但是值为undefined
2.然后执行代码
把GEC放入到ECS中,然后顺序执行代码,执行代码过程中,遇到变量、函数,会去GEC的VO中找,也就是GO中找
- 第一行,对GO中变量name赋值为captain
- 第二行,遇到foo,创建函数执行上下文(FEC),将FEC放入到ECS中,FEC中的VO就是AO,AO中存放的有num,但是值为undefined。然后顺序执行函数体内的代码,执行代码过程中,遇到变量、函数,会去FEC中的VO中找,也就是AO中找。对num进行赋值,执行console.log(name),这里的name在AO中没有找到,那么就从内往外,去父级VO中找,父级VO是GO,找到了,所以打印captain,至此foo函数执行完毕,对应的FEC出栈销毁
- 第七行,又遇到foo,同第二行,又重新创建函数执行上下文(FEC),剩下的过程同第二行的过程