在html文档中我们经常会通过在<script></stript>标签中添加我们的js代码,html文档在解析道script标签时,js引擎
就会解析我们的js代码,从而达到动态页面的效果,在这个过程中,js引擎会帮助我们做很多事情,来保证我们的代码能够正常运行,
这里面涉及到ECS(执行上下文栈),GEC(全局执行上下文),VO,GO(全局对象),AO,FEC,环境变量,环境记录,作用域提升,
作用域链等等这些概念,理解这些概念对我们编写js 代码有很大的帮助,特在此做梳理总结。
一:js代码的运行环境(原生js)
我们都知道使用node可以运行js文件,使用浏览器也可以运行我们编写的js文件,具体的原因是,在浏览器中和node 环境中
都嵌入了js引擎,是js 引擎为我们的js 代码的运行提供了保证。从普通使用者角度来说,不必细究这里的细节,但是作为一个前端
开发人员来说,当我们对js引擎所做的事情有更深的了解之后,就能更加容易的利用引擎的特性,来编写更高质量的js代码,毕竟了解
真相,才能获取真正的自由。
我个人认为js的代码运行环境,就是js引擎为我们创造的运行环境,在这里总结几个这个环境中的概念,以及这些概念与我们的
代码有什么关系做备忘录,辅助理解。
ECS(函数调用栈)&& GEC(全局执行上下文)- 作用域提升 && FEC(函数执行上下文)
1.执行上下文栈(调用栈)ECS
js引擎内部有一个执行上下文栈(Execution Context Stack,简称ECS),它是用于执行代码的调用栈。ECS 首先是一个栈,这个栈的作用就是执行放入到栈中的上下文,比如执行全局执行上下文GEC,执行函数执行上下文FEC。
2.GEC(全局执行上下文)- 作用域提升
全局的代码块为了执行代码会构建一个 Global Execution Context(GEC)。
GEC被放入到ECS中里面包含两部分内容:
(1)第一部分:在代码执行前,在parser转成AST的过程中,会将全局定义的变量、函数等加入到GlobalObject中,但是
并不会赋值;这个过程也称之为变量的作用域提升(hoisting)
(2)第二部分:在代码执行中,对变量赋值,或者执行其他的函数;
3.FEC(函数执行上下文):
js引擎在执行的过程中执行到一个函数时,就会根据函数体创建一个函数执行上下文(Functional Execution Context,
简称FEC),并且压入到EC Stack中。
FEC中包含三部分内容:
第一部分:在解析函数成为AST树结构时,会创建一个Activation Object(AO)
第二部分:作用域链:由VO(在函数中就是AO对象)和父级VO组成,查找时会一层层查找;
第三部分:this绑定的值:这个我会在后续的文章中详细解析;
VO && GO && AO
1.VO
早期ECMA规范这么写到: 每一个执行上下文会被关联到一个变量环境中(variable object,VO),在源代码中的变量
和函数声明会被作为属性添加到VO中。对于函数来说,函数也会被添加到VO中。VO是一个变量,在编译过程中会根据执行的内容做对应
赋值,在ECS中放入函数执行时时VO=AO,在ECS中放入GEC时,VO=GO。
2.GO
js引擎会在执行代码之前,会在堆内存中创建一个全局对象:Global Object(GO),该对象在所有的作用域(scope)都可以访问;
GO里面包含两部分内容:
(1)里面会包含Date、Array、String、Number、setTimeout、setInterval等等;
(2)其中还有一个window属性指向自己;
3.AO
在解析函数成为AST树结构时,会创建一个Activation Object(AO):
AO中包含形参、arguments、函数定义和指向函数对象、定义的变量;
环境变量(VE) && 环境记录(VR)
上面阐述的VO,GO,AO 其实是ECMA5之前或者ECMA5早期的概念,ECMA5后期及之后,ECMA修改了这几个概念,但大致意思
相差不多。
环境变量(VE)& 环境记录(VR)
每一个执行上下文会关联到一个变量环境中(Variable Environment)中,在执行代码中变量和函数的声明会作为环境记
录(Environment Record)添加到变量环境中。
对于函数来说,参数也被作为环境记录添加到变量环境中。
了解真相,才能获取真正的自由
了解完上述概念后,我们首先要有这么一个理解, 在script标签中我们可以创建一些内置对象,使用内置对象的方法,也可以
通过使用函数的功能,这些功能我们为什么能使用?
原因很简单,就是因为有GO,GO从哪里来?也很简单,是由js引擎在编译过程中给我们创建的。我们常说的js中只有全局
作用域和函数作用域,为什么,因为js引擎给我们创建了GO,AO,没有其他O。
最后,当了解完这些概念之后,我们还要探索在js代码运行的过程中,这些概念是如何联系起来的,这样能使我们对这些
概念的理解更上一层楼。
二: js引擎如何运行我们的代码?
背景知识:
在探索js引擎运行代码的逻辑之前,我们首先要对js的内存管理有一定的理解,否则在某些概念的理解上会很吃力。
有关概念,请移步[[js高级-内存管理]](https://juejin.cn/post/7089788055208329253)
js引擎运行代码的步骤
js 引擎要运行我们的代码就要经历三个阶段。具体如下
1.词法语法分析:词法语法分析就是检查JavaScript代码是否有一些低级的语法错误
2.预编译:下文详细讲解。
3.执行代码:执行代码就是js引擎解析代码,解析一行执行一行
第一个阶段需要编译的知识,这里我们主要研究和分析后两个阶段。另外,在理解作用域的的提升和作用域链的相关概念时,我们
一定要清晰的把第二个过程和第三个过程区分开,不然对这些概念的理解就会出现很大的偏差。
预编译阶段详解
预编译分为两个时间点
1.第一个是在JavaScript代码执行之前
2.第二个是在函数执行之前。
需要注意的是在JavaScript代码之前的时间点的预编译只发生一次,函数执行之前的预编译是多次的。
这里会将该段代码放在html文档结构的script标签中。
一:JavaScript代码执行之前的预编译
这个阶段主要做了如下几件事情
1.创建全局对象(GO),填充内容
填充的内容主要分为四个部分:
(1)Date、Array、String、Number、setTimeout、setInterval 等js内置的对象及其方法
(2)window,指向自身,并且可以在代码中全局访问
(3)解析代码,将其中的的全局变量,未使用和let生命的变量填充到GO,并将其赋值为undefined(作用域提升)
(4)解析代码,将其中的函数声明以key:value的形式,添加至GO中。(key:value形式存在,key为函数名,value为
函数体的内存地址)
2.创建全局执行上下文(GEC),并填充内容
填充的内容主要分为两部分
(1)填充变量环境(VO),这里的VO是一个变量,会将创建好的GO赋值给VO
(2)填充函数执行体,这里的函数执行体的解析是在函数执行前的预编译中进行的,在这个阶段中并不会填充这部分内容,
这里只是对GEC中的内容做结构展示
3.创建执行上下文栈(调用栈)ECS
创建执行上下文栈(调用栈)ECS,用来执行创建好的GEC。
二:函数执行前的预编译
1.创建AO对象
函数执行前的一瞬间,生成AO活动对象
2.分析生成AO属性
查找形参和变量声明放到AO对象,赋值为`undefined`
3.分析函数声明
查找函数声明放到AO对象并赋值为函数体。函数名为属性名,值为函数体;
4.生成函数执行上下文FEC,并填充内容
填充的内容主要分三个部分
(1)将AO对象作为VO,赋值给FEC的VO结构。
(2)填充作用域链:由VO(在函数中就是AO对象)和父级VO组成,查找时会一层层查找;
(3)this绑定的值:这个我会在后续文章中详细解析;
词法分析-编译-执行 流程总结
这里再对代码的运行流程做一些解释,即将执行的三个步骤和预编译的两个结合起来,做一个统一解释。
1.首先js代码的执行的第一步是做词法语法分析。
2.然后进行预编译的第一个时间点,即javaScript代码执行之前的预编译。
在这个时间点,进行如下几个步骤。
(1)创建全局对象(GO),填充内容
(2)创建全局执行上下文(GEC),并填充内容
(3)创建执行上下文栈(调用栈)ECS
3.接着开始执行代码,即从代码的第一行开始执行。
如果没有遇到函数,就一直执行。
4.如果遇到函数时,会来到预编译过程的第二个时间点,即函数执行之前的时间点。
在这个时间点会进行如下的几个步骤。
(1)将AO对象作为VO,赋值给FEC的VO结构。
(2)填充作用域链:由VO(在函数中就是AO对象)和父级VO组成,查找时会一层层查找;
(3)this绑定的值:这个我会在后续文章中详细解析;
5.执行函数体,知道该函数执行完毕
6.按照上述步骤继续执行全局代码,直到代码执行完毕。
下面根据我们总结的流程来对代码做详细的分析和画图总结
案例分析
var name = "why"
function foo(){
var name ='foo'
console.log(name)
}
var num1 = 20
var num2 = 30
var result = num1 + num2
console.log(result)
foo()
1.首先js代码的执行的第一步是做词法语法分析。这里会生成AST语法树。
2.然后进行预编译的第一个时间点,即javaScript代码执行之前的预编译。
(1)创建全局对象(GO),填充内容,具体填充如下几个内容
- Date、Array、String、Number、setTimeout、setInterval 等js内置的对象及其方法
- window,指向自身,并且可以在代码中全局访问
- 将name,num1,num2,result添加到GO对象中
- 将foo函数以foo:0xa00的形式赋值,添加到GO对象中
该步骤完成后会在堆中存在一个这样的对象:
(2)创建全局执行上下文(GEC),并填充内容
(3)创建执行上下文栈(调用栈)ECS
3.接着开始执行代码,即从代码的第一行开始执行。如果没有遇到函数,就一直执行。
4.如果遇到函数时,会来到预编译过程的第二个时间点,即函数执行之前的时间点。
5.执行函数体,知道该函数执行完毕
6.按照上述步骤继续执行全局代码,直到代码执行完毕,由于执行完foo函数后没有其他代码,所以js代码运行结束。
变量的作用域的提升
作用域的提升,发生在预编译的第一个阶段。
作用域链的生成
作用域链的生成发生在预编译的整个过程中