序言
以前自己是个 CV 仔,会点 ES6,能用 vue 和 ui 库就以为前端工作可以游刃有余。但其实只能完成基本业务罢了,想要查看一些深度一点的文章,都感觉读不进去,看看面经,哇,很多都是听不懂的词汇,看不懂的代码。于是,也开始沉下心来,慢慢下潜,了解 js 内部深处的奥妙。
V8 如何执行 JS
js 是解释型语言,机器是无法读懂的,需要通过 V8 引擎做翻译工作后才能执行,共分为以下步骤
1.生成AST
AST:即抽象语法树,编译器后续依靠 AST 进行工作。
这个部分分为词法分析和语法分析
- 词法分析:将一行行代码分解成一个个
token - 语法分析:将
token根据语法规则转化为AST这个部分还会生成执行上下文,下文会讲解。
2.生成字节码
字节码:介于 AST 和机器码之间的代码,比传统机器码更加轻量,通过解释器转换为机器码执行。
V8 的解释器在接收到 AST 后将其转换为 字节码,而不需要直接将全部字节码转换为机器码,降低内存压力。
3.执行代码
该阶段字节码被解释器逐行执行。并采用即时编译(JIT),提高执行效率。 即时编译,是将部分重复出现的代码打上热点标记,这部分代码由优化编译器提前编译为二进制。当再遇到这部分代码时,就直接执行编译而成的二进制,从而提高执行速率。
执行上下文
概念:Execution Context,即EC。其实就是 js 的执行环境,定义了变量或函数有权访问的其他数据没决定了他们各自的行为。js 引擎在执行代码前会进行预处理,根据可执行代码创建执行上下文。
可执行代码
共分为三种: - 全局执行代码:在代码执行前,解析创建全局执行上下文; - 函数执行代码:在执行函数前,解析创建函数上下文 - eval 执行代码:运行于当前执行上下文
执行上下文的组成
每一个执行上下文以三个属性组成 - 变量对象(Variable Object) 即 VO - 作用域链(Scope Chain) - this 后面会一一介绍,接下来看指向代码与执行上下文的关系
执行上下文栈
首先,js 运行环境存放在栈中,栈是一种先进后出的数据结构,也用来存放基本数据类型的值。
因此,js 引擎创建了执行上下文栈(Execution Context Stack),即ECS。通过 ECS 来管理每一个执行上下文。那么,又是如何管理的呢?
压栈与出栈
1.js开始解释执行代码,最开始是遇到全局执行代码
2.将其解析,创建全局上下文
3.将全局上下文压栈
4.逐行执行到一个函数时,解析创建函数执行上下文
5.将函数上下文压栈
6.函数执行后,将函数执行上下文弹栈,其中数据销毁
7.继续执行,直到应用结束
接下来,就是细看这个过程中,执行上下文的三个属性在压栈与出栈时,产生了怎样的变化。
变量对象
概念:是执行上下文的数据作用域,存储执行上下文定义的所有变量和函数声明。分为全局变量对象和函数变量对象
全局变量对象
全局变量对象(Global Variable Object) VO(G),其实就是我们常用的变量对象 特点: - VO(G) 在执行所有代码前创建 - VO(G) 存活到程序结束,对应全局上下文存在栈底 - VO(G) 保存全局上下文中所有变量和函数声明,保证可以访问全局对象的基础 - VO(G) 位于作用域链(执行上下文的另一个属性)的顶端
函数变量变量
一般用活动对象表示函数变量对象,活动变量对象(Activation Variable Object) AO。 活动对象在进入行数上下文创建,属于被激活的变量对象,通过函数的 arguments 属性初始化。
变量对象的两个阶段
- 代码解析阶段:在这个阶段,根据执行上下文的变量声明和函数声明来创建变量对象,
根据传入的参数度 arguments 赋值,为声明的值都为 undefined
- 代码执行阶段:按照顺序执行代码,修改变量对象的值。
作用域链
概念:在环境中执行代码时,会创建变量对象的作用域链,保证执行环境(EC)有权访问所有变量和函数的有序访问。
特点:
- 本质是执行边来那个对象的指针
- 前端指向当前执行上下文的变量对象
- 末端对应全局执行上下文的全局变量对象
创建阶段
全局阶段
首先在执行全局代码前,我们会先创建全局上下文。创建全局上下文的第一步是创建全局变量对象,
然后将全局变量对象放入作用域链的顶端(执行上下文中的[[Scope]]属性指向作用域链)
函数阶段
函数上下文首先会复制函数的[[Scope]]属性用来创建作用域链,然后用 arguments 创建活动对象,
最后再将活动对象压入作用域链顶端
this
概念
this 是指针,指向最后一次调用这个方法的对象。
指向
- 方法调用,绑定到该方法所属对象
- 函数调用,绑定到全局函数
- 构造器调用,绑定到实例
- apply,vall,bind,绑定到传入对象