前言
我们先不看这个标题,来看下面这段代码是怎么运行的:
var num1=1 var num1=2 var result=num1+num2 console.log(result)
我相信,小伙伴都知道打印的是3把,这也太简单了,但是我们思考下,这段代码执行时,到底进行了怎样的操作,这里我们要引进一个概念:初始化全局对象和执行上下文
初始化全局对象(GO)
JS引擎在解析代码的时候,会在堆内存中创建一个全局对象叫:Gobal Object(GO)
- 该对象所有的作用域都可以访问
- 里面会包含Date Array String Number setTimeout setInterval 等等
- 还有一个windows属性指向本身
执行上下文
代码运行时:js引擎(这里考虑V8引擎)会创建执行上下文栈(Execution context stack)简称ECS,它就是执行代码的调用栈
执行上下文分为:
- 全局执行上下文( GEC )
- 函数执行上下文( FEC )
这里我们先分析下全局执行上下文,因为我们开头那段代码是全局代码
全局执行上下文
在执行全局代码的时候,会创建全局执行上下文( GEC ),GEC会被放入ECS中去执行 GEC被放入ECS
包含两部分:
- 第一部分:在代码执行前,也就是V8引擎将parser转成AST的过程中 ( 这里会涉及到V8引擎在解析js代码的过程,会开篇专题讲这个),会将全局定义的变量放入GO中,但是并不会赋值,因为是解析阶段 (pass:这个过程也被叫做变量的作用域提升)
- 第二部分:执行代码,并为GO里面的变量赋值,或者执行其他函数 流程图如下:图中执行前是上面描述的第一部分,开始执行代码是第二部分
Java Script遇到函数代码如何执行?
当js引擎遇到函数执行时,会创建一个 函数执行上下文( Functional Execution Contex ) 简称FEC,并压入到执行上下文栈ECS
FEC中包含三部分内容:
- 第一部分:VO(variable object ) 对象,其实也是AO( Activation Object )
- 第二部分:作用域链(scope chain):由自己的VO+父级的VO,查找时会一层一层去查找
- 第三部分:this绑定( 这里不做详细描述,后续会出专题 )
作用域是解析编译的时候就决定了,并不是执行调用的时候来我们看一段代码,一起看一下函数的执行过程,一起来思考下这段代码的运行结果
var message = "Global" function foo() { console.log(message) } function bar() { var message = "Bar" foo() } bar()
画个图分析一下吧
首先初始化全局对象GO
,里面有Array、date、setTime
等等,还有自己定义的 message对象初始值为undefined,foo和bar函数对象
开始执行代码前,会创建一个执行上下文栈ESC
,开始执行全局代码,所以会创建一个全局执行上下文栈GEC
,将GEC压入栈底,全局执行上下文包括两部分:
第一部分代码执行前的VO对象
(这里的VO指向的是GO
)
第二部分是开始执行代码,执行第一行时 var message="Global"
,GO对象里的message就被改为Global
执行完第一行后,开始执行第9行代码 bar(),这里是函数的调用执行,js引擎会创建一个函数执行上下文FEC,压入栈中,FEC包含三部分:
第一部分:VO对象
,这里指向bar自己的AO对象(包括形参和函数中定义的变量
)
第二部分:作用域链scope chain
,自己的VO对象+parent VO对象
第三部分:this绑定
,这里是指向windows(这个后续会开专题讲)
开始执行bar函数里面的代码,var message="Bar"
,会将bar的AO对象的message变量值从undefined改为"Bar"
,接下来在执行foo()函数,注意:此时bar函数还没弹出栈,因为foo函数还在执行
访问一个变量的时候,会沿着作用域链一层一层往上找,最后没有找到则会报错
执行到bar函数的最后一行代码是foo(),此时又是一个函数的调用执行,又会创建一个foo的函数执行上下文,也包含上述的三部分:VO对象(指向foo的AO对象) 作用域链 和this绑定
开始执行foo函数代码console.log(message)
,当打印message的时候,会沿着作用域链一层层找
,foo的作用域链是自己的AO+父级的VO(也就是GO对象)
,自己的AO对象为空,所以找到GO里面的message变量,最终打印"Global"
执行完后,函数执行上下文执行完之后,就会弹出栈,foo的FEC先弹出栈,然后bar的FEC弹出栈,他们自个的AO对象最终也被释放
环境变量和记录
我们上述所说的VO是基于早期的ECMAS规范,官网是这样说的:
这里借助coderwhy的翻译:每个执行上下文都关联一个变量对象(Vriable Object
),在源代码中变量和函数的声明会被作为属性加入到变量对象中,对于函数来说,参数也会被加入到VO中但是ECMA5以后的版本,官网做了一些词汇用语的修改
每个执行上下文都会关联一个变量环境(variable Environment)
,在执行代码中变量和函数的声明会被当做环境记录(Environment Record)
加入到变量环境中。
对于函数代码,形参也会被当做环境记录加入到变量环境中
总结:
- 在访问一个变量时,会沿着作用域链一层层往上找,最终没找到,会报错:未定义
- 作用域以及父级作用域是代码在编译阶段就已经确定了,和调用位置没关系
汇总一些名词解释,我们来解释一下:
名词 | 解释 |
---|---|
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),VO被激活就变成了AO,VO和AO本质上是一个东西,它们其实都是同一个对象,只是处于执行环境的不同生命周期 |
到此这篇关于深入学习JavaScript执行上下文的文章就介绍到这了,更多相关JS执行上下文内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!