深入JavaScript执行原理
深入V8引擎原理
JavaScript代码的执行
JavaScript代码下载好之后,是如何一步步被执行的呢?我们知道,浏览器内核是由两部分组成的,以webkit为例:
WebCore
:负责HTML解析、布局、渲染等等相关的工作;JavaScriptCore
:解析、执行JavaScript代码;
另外一个强大的引擎就是V8引擎
V8引擎的执行原理(了解)
官方对v8引擎的定义:
- V8是用
C++编写的
Google开源高性能JavaScript和WebAssembly引擎
,它用于Chrome和Node.js
等。 - 它实现
ECMAScript
和WebAssembly
,并在Windows7或更高版本,macOS
10.12+和使用x64,IA-32,ARM或MIPS处理器的Linux系统上运行。 V8可以独立运行,也可以嵌入到任何C++应用程序
中。
-
当我们的一段js代码需要进行执行时:
- V8引擎首先会使用解析器(编译器)(Parse模块)经过词法分析 > 语法分析 这几步生成一颗抽象语法树(AST) ,由于解释器(Ignition模块)是不能直接认识js代码的所以需要经过编译器将js代码生成AST 。当需要进行代码转换时,例如es6,ts代码需要转换成es5的时候,可以访问AST树,将AST树转换为es5规定的代码,然后再生成新的AST树parse的V8官方文档
- 抽象语法树在由解释器(Ignition模块)生成字节码(伪汇编代码),生成的字节码可以跨平台使用,在浏览器和node环境下都是可以用字节码直接生成所需要的的结果Ignition的V8官方文档
- 字节码接着执行接下来的步骤真正的汇编代码 =>机器码 => cpu执行,这就是当你在调用相关的js函数时,V8引擎需要执行的步骤(上面三步主要由解释型语言实现的)
4. 如果一个函数被多次调用,那么就会被标记为热点函数,那么就会经过TurboFan转换成优化的机器码,不需要在经过步骤三就可以直接被CPU执行,提高代码的执行性能(这一步主要由C++实现),这里有一个需要注意的是:以上面的代码为例,多次调用sum函数,并且传入的参数类型都是相同类型时(列如number类型),这一步骤才会生效,如果我们这时改变一个参数的类型,之前优化的机器码并不能正确的处理运算,就会逆向的转换成字节码,这样做会让性能大打折扣,如果在使用typescript进行类型检测,是可以提高一定的性能TurboFan的V8官方文档
JavaScript执行过程
初始化全局对象
执行上下文
什么是执行上下文
- js引擎内部有一个执行上下文栈(Execution Context Stack,简称ECS),它是用于
执行代码的调用栈
。 - 当 JS 引擎
解析到可执行代码片段
(通常是函数调用阶段)的时候,就会先做一些执行前的准备工作
,这个 “准备工作”,就叫做"执行上下文
(execution context 简称 EC)" 或者也可以叫做执行环境。
执行上下文的类型
javascript 中有三种执行上下文类型,分别是:
- 全局执行上下文——这是默认或者说是最基础的执行上下文,一个程序中只会存在一个全局上下文,它在整个 javascript
脚本的生命周期内都会存在于执行堆栈的最底部不会被栈弹出销毁。全局上下文会生成一个全局对象(以浏览器环境为例,这个全局对象是
window),并且将 this 值绑定到这个全局对象上。 - 函数执行上下文——每当一个函数被调用时,都会创建一个新的函数执行上下文(不管这个函数是不是被重复调用的)
- Eval 函数执行上下文—— 执行在 eval 函数内部的代码也会有它属于自己的执行上下文,但由于并不经常使用
eval,所以在这里不做分析。
全局代码执行前后的流程
- 全局的代码块为了执行会构建一个Global Execution Context (GEC) ;
- GEC会
被放入到ECS中
执行;
GEC被放入到ECS中里面包含两部分内容:
VO对象(Variable Object)
每一个执行上下文会关联一个VO (Variable object,变量对象
),变量和函数声明
会被添加到这个VO对象中。
全局代码执行过程(执行后)示例:
函数代码执行过程
在执行的过程中执行到一个函数时
,就会根据函数体
创建一个函数执行上下文
(Functional Execution Context,简称FEC)并且压入到EC Stack
中。
作用域链
什么是作用域
简单来说,作用域(英文:scope)是据名称来查找变量的一套规则,可以把作用域通俗理解为一个封闭的空间,这个空间是封闭的,不会对外部产生影响,外部空间不能访问内部空间,但是内部空间可以访问将其包裹在内的外部空间。
[[Scopes]]属性
- 在javascript中,每个函数都是一个对象,在对象中有些属性我们可以访问,有些我们是不能自由访问的,[[Scopes]]属性就是其中之一,这个属性只能被JavaScript引擎读取。
- 其实[[scope]]就是我们常说的作用域,其中存储了作用域运行期的上下文集合。
- 在这里因为func.prototype.constructor和func指向同一个函数,所以在这里我们通过访问函数func的原型对象来查看[[Scopes]]属性
作用域链
[[scope]]中存储的执行期的上下文对象的集合
,这个集合呈链式连接
,我们把这种链式连接叫做作用域链
。JavaScript正是通过作用域链来查找变量的,其查找方式是沿着作用域链的顶端依次向下查询(在哪个函数内部查找对象,就在哪个函数作用域链中查找)
变量查找的作用域链顺序
以下面的例子为例,结合着之前讲的变量对象和执行上下文栈,我们来总结一下函数执行上下文中作用域链和变量对象的创建过程:
var Scope = "global Scope";
function checkScope(){
var Scope2 = 'local Scope';
return Scope2;
}
checkScope();
执行过程如下:
1.checkScope 函数被创建,保存作用域链到 内部属性[[Scope]]
checkScope.[[Scope]] = [ globalContext.VO];
2.执行 checkScope 函数,创建 checkScope 函数执行上下文,checkScope 函数执行上下文被压入执行上下文栈
ECStack = [
checkScopeContext,
globalContext
];
3.checkScope 函数并不立刻执行,开始做准备工作,第一步:复制函数[[Scope]]属性创建作用域链
checkScopeContext = {
Scope: checkScope.[[Scope]],
}
4.第二步:用 arguments 创建活动对象,随后初始化活动对象,加入形参、函数声明、变量声明
checkScopeContext = {
AO: { arguments: { length: 0 }, Scope2: undefined }, Scope: checkScope.[[Scope]],
}
5.第三步:将活动对象压入 checkScope 作用域链顶端
checkScopeContext = {
AO: { arguments: { length: 0 }, Scope2: undefined }, Scope: [AO, [[Scope]]]
}
6.准备工作做完,开始执行函数,随着函数的执行,修改 AO 的属性值
checkScopeContext = {
AO: { arguments: { length: 0 }, Scope2: 'local Scope' }, Scope: [AO, [[Scope]]]
}
7.查找到 Scope2 的值,返回后函数执行完毕,函数上下文从执行上下文栈中弹出
ECStack = [
globalContext
];