Skip to content

JavaScript 引擎 #45

@coconilu

Description

@coconilu

概述

JavaScript引擎是一个专门处理JavaScript脚本的虚拟机,它是根据ECMAScript规范来实现的,所以工程师可以编写符合ECMAScript规范的JS代码并运行在浏览器上。

根据MDN说明,JavaScript ( JS ) 是一种具有函数优先的轻量级解释型或即时编译型的编程语言。

JS引擎会以执行上下文为一个执行单元去解析执行代码。一个执行上下文可以是全局代码,也可以是函数代码,相对应的有全局作用域和函数作用域。解析代码的时候会触发变量提升。

但是,为了优化执行效率,JavaScriptCore引擎和V8引擎会带有JIT编译器,JIT会把执行超过一次的代码块编译并保存好,下次再次执行的时候会跳过翻译直接执行。这个代码块还会被继续观察并尝试更多的优化并保存好。在编译器进行优化的过程中会做一些关于变量类型和环境中值的假设;但是如果假设不成立就将这个优化的版本回退,如果假设成立的话,这将让代码性能更高。

所以得出结论,JS引擎是解释型语言,但是有些版本的JS引擎为了优化执行效率会带有JIT即时编译器。

解释型语言和编译型语言

编译型语言是代码在运行前编译器将人类可以理解的语言(编程语言)转换成机器可以理解的语言。

解释型语言也是人类可以理解的语言(编程语言),也需要转换成机器可以理解的语言才能执行,但是是在运行时转换的。

解释型语言运行慢的原因是,每次执行都需要把源码转换一次才可以执行。

编译优化代码阶段

  1. 词法分析,任务是识别源程序中的单词是否有误
  2. 语法分析,目的是识别出源程序的语法结构(即短语、句子、过程、程序),所以有时又称为句子分析
  3. 语义分析,检查程序的语义正确性,包括检查类型
  4. 中间代码产生,产生介于源语言与目标代码之间的一种中间代码,抽象语法树
  5. 优化,对代码进行加工转化,包括删除无用语句、循环优化
  6. 目标代码生成,转化成特定机器上的低级语言代码

涉及的数据结构有:语法树、符号表、常数表

JavaScriptCore

JavaScriptCore 执行 一系列步骤 来解释和优化脚本:

  1. 它进行词法分析,就是将源代码分解成一系列具有明确含义的符号或字符串。
  2. 然后用语法分析器分析这些符号,将其构建成语法树。
  3. 接着四个 JIT(Just-In-Time)进程开始参与进来,分析和执行解析器所生成的字节码。

V8

Google 的 V8 引擎 是用 C++ 编写的,它也能够编译并执行 JavaScript 源代码、处理内存分配和垃圾回收。它被设计成由两个编译器组成,可以把源码直接编译成机器码:

  1. Full-codegen:输出未优化代码的快速编译器
  2. Crankshaft: 输出执行效率高、优化过的代码的慢速编译器

如果 Crankshaft 确定需要优化的代码是由 Full-codegen 生成的未优化代码,它就会取代 Full-codegen,这个过程叫做“crankshafting”。

关键概念

1. 堆栈

堆是存储对象的地方;栈是存放一个又一个执行上下文的地方,函数是运行的基本单位,换句话说,一个函数对应一个上下文,一个作用域。
参考下图:
js

2. 执行上下文

JS引擎刚开始读取JS文件的时候,会创建一个全局上下文,并把它放到执行上下文栈里,之后如果遇到需要新创建执行上下文,会把它压到栈顶;直到相关的代码执行完毕并返回结果,该执行上下文会被弹出栈。执行上下文栈是创建作用域链的重要参考。

3. 变量对象

变量对象指的是每个执行上下文可以访问的变量合集。

全局执行上下文的变量对象有:

  1. 全局对象(浏览器环境中是window,nodejs中是global),允许直接访问属性(比如经常使用的new Date,本身就是Window.Date)
  2. 使用或不使用var定义的变量,差别是不使用var定义的变量可以用delete操作符删除
  3. 函数声明

函数执行上下文的变量对象有:

  1. arguments
  2. 实参
  3. 使用var定义的变量
  4. 函数声明

4. 作用域与作用域链

ES6之前没有块作用域,仅有全局作用域和函数作用域。

当前作用域没有定义的变量,这成为自由变量 。自由变量如何得到 —— 向父级作用域寻找。

比如:在函数执行上下文中未使用var定义的变量被调用的时候,因为在变量对象中找不到,所以会顺着作用域链查找下一个执行上下文中的变量对象。如果在全局作用域里也找不到,将会报错not defined

自由变量将从作用域链中去寻找,但是依据的是函数定义时的作用域链,而不是函数执行时

作用域链是根据执行上下文栈来确定的。

特殊作用域:

  1. try-catch语句块,临时在作用域链中添加error对象
  2. with语句块,也会临时在作用域链中添加with的对象
  3. eval语句,如果你间接的使用 eval(),比如通过一个引用来调用它,而不是直接的调用 eval ,从 ECMAScript 5 起,它工作在全局作用域下,而不是局部作用域中

5. 闭包

刚刚提到:自由变量将从作用域链中去寻找,但是依据的是函数定义时的作用域链,而不是函数执行时

闭包就是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。

遮蔽,自由变量会被函数的局部变量遮蔽,遮蔽的自由变量会访问不到。

闭包就是将函数内部和函数外部连接起来的一座桥梁。

为什么会用到高阶函数?粗糙的说,就是为了闭包。

6 提升

提升是相对于var声明的变量和函数声明的,函数表达式并不会被提升。

提升的伪逻辑如下:

  1. 提升var变量,如果存在同名变量(包括同名形参),不进行操作;若不存在,则初始化为undefined
  2. 提升函数声明,提升并覆盖已有的同名变量

7. this的指向

this刚好和闭包相反,确定this是在函数执行时,而不是函数定义时

一旦确定了this的指向,当访问this的某个成员变量或函数,如果不存在不会根据执行上下文栈找到上一个this并查询。

1. 全局执行上下文

this指向全局对象,在浏览器环境下是window,在nodejs下是global

2. 函数执行上下文

  1. 作为对象的方法,即使方法是来自该对象的原型链上的方法,this也指向该对象
  2. 作为构造函数,指向正在构建的新对象
  3. 如果是简单调用,就是形如fun()而不是obj.fun(),this指向全局对象,但是在严格模式下,会指向undefined
  4. 箭头函数,与封闭词法上下文的this保持一致

这里的封闭词法上下文没有那么高大上,说白了,箭头函数的执行上下文没有定义this属性,根据执行上下文栈找到上一个执行上下文的this,就是箭头函数中的this。

场景

  1. 作为构造函数执行,构造函数中,this指向新对象
  2. 作为对象属性执行,指向这个对象
  3. 作为普通函数执行,指向全局对象
  4. 用于call apply bind,绑定了的对象

8. 暂时性死区

只有在es6中的let和const才会发生这种情况。

进入一个执行上下文的时候,
let、const声明的变量和形参定义的变量,都会屏蔽掉前一个执行上下文中的同名变量,
且在声明语句之前是不可以使用该变量,
声明语句之后,若该变量没有被赋值,则默认为undefined。

参考

认识 V8 引擎
一篇给小白看的 JavaScript 引擎指南
JS工作原理:引擎,运行时和调用堆栈
V8基础学习一:从编译流程学习V8优化机制
JavaScript到底是解释型语言还是编译型语言?
什么是 JavaScript?
了解编译原理-笔记小结
编译原理学习笔记一

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions