目录
ECMAJavaScript版本说明
在不同的ECMAJavaScript
版本中对于js
的执行原理
,相关术语
都有一些差别
但事实上,他们只是在某些概念上的描述不一样,具体的流程
是一样的
这里通过ECMAJavaScript3
中的概念来学习JavaScript执行原理
浏览器内核
我们的浏览器大致由两部分组成
以webkit
内核为例
WebCore
负责HTML
的解析,布局,渲染等等工作
关于浏览器的渲染原理
可以看我以下这篇文章
浏览器渲染原理
JavaScriptCore
负责JavaScript
代码的解析执行
以V8引擎
为例
V8引擎执行原理
V8
是使用C++
编写的谷歌开源
高性能JavaScript
引擎,用于Chrome
和nodejs
中
V8
可以独立运行,也可以嵌入任何C++
应用程序中
在V8
中运行的代码,会经历以下几个步骤
Parse
在最开始,我们的JavaScript
代码会先被进行词法分析
(lexical analysis),将字符串转换成token
序列的形式
接下来将对其进行语法分析
(Parser)并生成AST抽象语法树
如果函数没有被调用的话是不会出现在AST抽象树中的
Parse
的V8
官方文档:Parse
有关于AST抽象语法树
相关的内容可以看我这一篇文章
(未动笔,未来可寄)
Ignition
Ignition
是一个解释器
,负责将AST抽象语法树
转换为字节码
同时会收集TurboFan
所需要的相关信息,例如函数的运行信息
等等
如果函数只会执行一次
,那么Ignition
会解释执行
字节码
如果函数需要执行多次
,就会进入TurboFan
环节
Ignition
的V8
官方文档:Ignition
TurboFan
TurboFan
是一个编译器
,负责将字节码编译成机器码
TurboFan
主要负责对代码的执行进行优化
如果当一个函数被调用多次时,这个函数将会被标记为热点函数
被标记为热点函数的代码块会直接由TurboFan
编译为机器码
执行,不再经过字节码执行
TurboFan
的V8
官方文档:TurboFan
Deoptimization
当函数的参数类型发生了变化
,如需要传入一个Number
类型的数据,传进去了String
类型的数据
之前优化的机器码就无法使用了,这时就会反向优化成字节码
,交给Ignition
执行
执行上下文
js引擎
内部有一个执行上下文栈
(Execution Context Stack,简称ECS
),它是用于执行代码的调用栈
全局代码块
会入栈一个全局执行上下文
(Global Execution Context,简称GEC
)
而如果是函数代码块
的话就会入栈一个函数执行上下文
(Functional Execution Context,简称FEC
)
当栈顶的上下文执行完毕后该上下文就会出栈
,js引擎继续执行下一个上下文
GO对象
js引擎
会在代码执行前,在堆内存
中创建一个全局对象
:Global Object(简称GO
)
所有的作用域都能访问到这个对象,即所有对象的作用域链
中都包含这个对象
全局对象中包含许多属性,如setTimeOut
,Number
,String
等等
其中有一个window
属性指向自身
FO对象
全局代码
会在堆内存中创建一个GO
当遇到函数代码
时js引擎也会创建一个函数对象
(Function Object,简称FO
)
FO
中同样存放着一些属性,如name
,length
等等
还存放着函数体的代码
以及作用域链
([[scopes]]
)
VO对象
无论是全局执行上下文
还是函数执行上下文
都需要关联一个变量对象
(Variable Object,简称VO
)
VO用于存放变量和函数的声明
VO不一定是新创建的对象
在全局执行上下文
中的VO
就是GO
而在函数执行上下文
中的VO
就需要重新创建
AO对象
因为每个执行上下文都需要关联一个VO
全局执行上下文
关联的VO
是GO
函数执行上下文
就需要创建一个AO
(Activation Object)来关联到VO
AO
会使用arguments
作为初始化,并且值为传入的参数
arguments
为一个对象列表
AO
会用来存放变量的初始化
作用域提升
无论是全局执行上下文
还是函数执行上下文
在代码执行前都会将定义的变量,函数等加入到对应的VO中
但此时并不会赋值
如果当函数与变量重名时变量
会被保留
这个过程被称之为作用域提升
作用域和作用域链
当进入到一个执行上下文中时,执行上下文会关联一个作用域链
(Scope Chain,简称SC
)
SC
是一个对象列表
,里面存放一系列的对象
当需要查找值时如果本级作用域查找不到就会按照SC
中的顺序来查找
SC和VO一样不一定是新创建的对象
全局执行上下文
的SC
就是本身(this
)
而函数执行上下文
的SC
则关联到了其对应FO
中的[[scopes]]
属性
演示
以这一段代码来演示
var a = 0;
var b = 1;
function foo() {
console.log(a)
var a = 2;
}
function bar(age) {
function baz() {
console.log(age)
}
return baz
}
foo()
var func = bar(18)
func()
初始化
此时代码还没运行,js引擎
开始初始化,如生成GO
,FO
等等
开始执行全局代码
执行到第二行,此时a
与b
均被赋值
执行函数
开始调用foo
函数,创建一个FEC
和一个AO
接下来会运行foo
里的代码
第一行为输出a
的值,因为此时a
还尚未赋值,所以控制台会输出undefined
,第二行进行赋值操作
运行完毕后,FEC
会出栈,此时GEC
重新成为栈顶,继续执行全局代码
相关AO
内存也会因为js的垃圾回收机制
被回收
关于js的垃圾回收机制
可以看我这篇文章
闭包与垃圾回收
此时开始执行bar
函数,并将函数的返回值赋给func
在bar
函数执行完后,FEC
会出栈,其对应的AO
也本应该被回收,但因为这里形成了闭包
,所以js的垃圾回收机制
无法回收这部分内存
关于js闭包
相关内容可以看我这篇文章
(未动笔,未来可寄)
最后执行func
函数,因为bar
的AO
对象没有被回收,所以baz
能访问到bar
中的age
最后看看控制台的打印结果