在不同的ECMAJavaScript
版本中对于js
的执行原理
,相关术语
都有一些差别
但事实上,他们只是在某些概念上的描述不一样,具体的流程
是一样的
这里通过ECMAJavaScript3
中的概念来学习JavaScript执行原理
我们的浏览器大致由两部分组成
以webkit
内核为例
负责HTML
的解析,布局,渲染等等工作
关于浏览器的渲染原理
可以看我以下这篇文章
浏览器渲染原理
负责JavaScript
代码的解析执行
以V8引擎
为例
V8
是使用C++
编写的谷歌开源
高性能JavaScript
引擎,用于Chrome
和nodejs
中
V8
可以独立运行,也可以嵌入任何C++
应用程序中
在V8
中运行的代码,会经历以下几个步骤
在最开始,我们的JavaScript
代码会先被进行词法分析
(lexical analysis),将字符串转换成token
序列的形式
接下来将对其进行语法分析
(Parser)并生成AST抽象语法树
如果函数没有被调用的话是不会出现在AST抽象树中的
Parse
的V8
官方文档:Parse
有关于AST抽象语法树
相关的内容可以看我这一篇文章
(未动笔,未来可寄)
Ignition
是一个解释器
,负责将AST抽象语法树
转换为字节码
同时会收集TurboFan
所需要的相关信息,例如函数的运行信息
等等
如果函数只会执行一次
,那么Ignition
会解释执行
字节码
如果函数需要执行多次
,就会进入TurboFan
环节
Ignition
的V8
官方文档:Ignition
TurboFan
是一个编译器
,负责将字节码编译成机器码
TurboFan
主要负责对代码的执行进行优化
如果当一个函数被调用多次时,这个函数将会被标记为热点函数
被标记为热点函数的代码块会直接由TurboFan
编译为机器码
执行,不再经过字节码执行
TurboFan
的V8
官方文档:TurboFan
当函数的参数类型发生了变化
,如需要传入一个Number
类型的数据,传进去了String
类型的数据
之前优化的机器码就无法使用了,这时就会反向优化成字节码
,交给Ignition
执行
js引擎
内部有一个执行上下文栈
(Execution Context Stack,简称ECS
),它是用于执行代码的调用栈
全局代码块
会入栈一个全局执行上下文
(Global Execution Context,简称GEC
)
而如果是函数代码块
的话就会入栈一个函数执行上下文
(Functional Execution Context,简称FEC
)
当栈顶的上下文执行完毕后该上下文就会出栈
,js引擎继续执行下一个上下文
js引擎
会在代码执行前,在堆内存
中创建一个全局对象
:Global Object(简称GO
)
所有的作用域都能访问到这个对象,即所有对象的作用域链
中都包含这个对象
全局对象中包含许多属性,如setTimeOut
,Number
,String
等等
其中有一个window
属性指向自身
全局代码
会在堆内存中创建一个GO
当遇到函数代码
时js引擎也会创建一个函数对象
(Function Object,简称FO
)
FO
中同样存放着一些属性,如name
,length
等等
还存放着函数体的代码
以及作用域链
([[scopes]]
)
无论是全局执行上下文
还是函数执行上下文
都需要关联一个变量对象
(Variable Object,简称VO
)
VO用于存放变量和函数的声明
VO不一定是新创建的对象
在全局执行上下文
中的VO
就是GO
而在函数执行上下文
中的VO
就需要重新创建
因为每个执行上下文都需要关联一个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
函数执行完后,FEC
会出栈,其对应的AO
也本应该被回收,但因为这里形成了闭包
,所以js的垃圾回收机制
无法回收这部分内存
关于js闭包
相关内容可以看我这篇文章
(未动笔,未来可寄)