【JS引擎】解析过程 |8月更文挑战

498 阅读9分钟

目录

  1. JS是解释型语言
  2. 引擎对JS的处理词法分析 -> 得到token -> 语法分析 -> 得到AST -> 翻译器 -> 字节码(bytecode) -> 字节码解释器 -> 机器码
  3. JS的预处理阶段 【分号不全,变量提升】
  4. JS的执行阶段 【执行上下文, VO(变量对象)和AO(活动对象),作用域链】 在函数上下文中:VO === AO 在全局上下文中:VO === this === global
  5. 垃圾回收机制GC【标记清除】
  6. 参考
  7. 总结

一 JS是解释型语言

JS是解释型语音,所以它无需提前编译,而是由解释器实时运行

JS需要引擎解析

二 引擎对JS的处理过程可以简述如下:

1. 读取代码,进行词法分析(Lexical analysis),然后将代码分解成词元(token)

2. 对词元进行语法分析(parsing),然后将代码整理成语法树(syntax tree)

3. 使用翻译器(translator),将代码转为字节码(bytecode)

4. 使用字节码解释器(bytecode interpreter),将字节码转为机器码

词法分析 -> 得到token -> 语法分析 -> 得到AST -> 翻译器 -> 字节码(bytecode) -> 字节码解释器 -> 机器码

最终计算机执行的就是机器码。

为了提高运行速度,现代浏览器一般采用即时编译(JIT-Just In Time compiler

即字节码只在运行时编译,用到哪一行就编译哪一行,并且把编译结果缓存(inline cache

这样整个程序的运行速度能得到显著提升。

而且,不同浏览器策略可能还不同,有的浏览器就省略了字节码的翻译步骤,直接转为机器码(如chrome的v8)

总结起来可以认为是: 核心的JIT编译器将源码编译成机器码运行

三 JS的预处理阶段

1 分号补全

2 变量提升

上述将的是解释器的整体过程,这里再提下在正式执行JS前,还会有一个预处理阶段 (譬如变量提升,分号补全等)

预处理阶段会做一些事情,确保JS可以正确执行,这里仅提部分:

分号补全

JS执行是需要分号的,但为什么以下语句却可以正常运行呢?

console.log('a')
console.log('b')

原因就是JS解释器有一个Semicolon Insertion规则,它会按照一定规则,在适当的位置补充分号

譬如列举几条自动加分号的规则:

  • 当有换行符(包括含有换行符的多行注释),并且下一个token没法跟前面的语法匹配时,会自动补分号。
  • 当有}时,如果缺少分号,会补分号。
  • 程序源代码结束时,如果缺少分号,会补分号。

于是,上述的代码就变成了

console.log('a');
console.log('b');

所以可以正常运行

当然了,这里有一个经典的例子:

function b() {
    return
    {
        a: 'a'
    };
}

由于分号补全机制,所以它变成了:

function b() {
    return;
    {
        a: 'a'
    };
}

所以运行后是undefined

变量提升

一般包括函数提升和变量提升

譬如:

a = 1;
b();
function b() {
    console.log('b');
}
var a;

经过变量提升后,就变成:

function b() {
    console.log('b');
}
var a;
a = 1;
b();

这里没有展开,其实展开也可以牵涉到很多内容的

譬如可以提下变量声明,函数声明,形参,实参的优先级顺序,以及es6中let有关的临时死区等

四 JS的执行阶段

此阶段的内容中的图片来源:深入理解JavaScript系列(10):JavaScript核心(晋级高手必读篇)

解释器解释完语法规则后,就开始执行,然后整个执行流程中大致包含以下概念:

  • 执行上下文,执行堆栈概念(如全局上下文,当前活动上下文)
  • VO(变量对象)和AO(活动对象)
  • 作用域链
  • this机制等

这些概念如果深入讲解的话内容过多,因此这里仅提及部分特性

执行上下文简单解释

  • JS有执行上下文
  • 浏览器首次载入脚本,它将创建全局执行上下文,并压入执行栈栈顶(不可被弹出)
  • 然后每进入其它作用域就创建对应的执行上下文并把它压入执行栈的顶部
  • 一旦对应的上下文执行完毕,就从栈顶弹出,并将上下文控制权交给当前的栈。
  • 这样依次执行(最终都会回到全局执行上下文)

譬如,如果程序执行完毕,被弹出执行栈,然后有没有被引用(没有形成闭包),那么这个函数中用到的内存就会被垃圾处理器自动回收

然后执行上下文与VO,作用域链,this的关系是:

每一个执行上下文,都有三个重要属性:

  • 变量对象(Variable object,VO)
  • 作用域链(Scope chain)
  • this

VO与AO

VO是执行上下文的属性(抽象概念),但是只有全局上下文的变量对象允许通过VO的属性名称来间接访问(因为在全局上下文里,全局对象自身就是变量对象)

AO(activation object),当函数被调用者激活,AO就被创建了

可以理解为:

  • 在函数上下文中:VO === AO
  • 在全局上下文中:VO === this === global

总的来说,VO中会存放一些变量信息(如声明的变量,函数,arguments参数等等)

作用域链

它是执行上下文中的一个属性,原理和原型链很相似,作用很重要。

譬如流程简述:

在函数上下文中,查找一个变量foo

如果函数的VO中找到了,就直接使用

否则去它的父级作用域链中(__parent__)找

如果父级中没找到,继续往上找

直到全局上下文中也没找到就报错

this指针

这也是JS的核心知识之一,由于内容过多,这里就不展开,仅提及部分

注意:this是执行上下文环境的一个属性,而不是某个变量对象的属性

因此:

  • this是没有一个类似搜寻变量的过程
  • 当代码中使用了this,这个 this的值就直接从执行的上下文中获取了,而不会从作用域链中搜寻
  • this的值只取决中进入上下文时的情况

所以经典的例子:

var baz = 200;
var bar = {
    baz: 100,
    foo: function() {
        console.log(this.baz);
    }
};
var foo = bar.foo;

// 进入环境:global
foo(); // 200,严格模式中会报错,Cannot read property 'baz' of undefined

// 进入环境:global bar
bar.foo(); // 100

就要明白了上面this的介绍,上述例子很好理解

更多参考:

深入理解JavaScript系列(13):This? Yes,this!

五 垃圾回收机制GC【标记清除】

JS有垃圾处理器,所以无需手动回收内存,而是由垃圾处理器自动处理。

一般来说,垃圾处理器有自己的回收策略。

譬如对于那些执行完毕的函数,如果没有外部引用(被引用的话会形成闭包),则会回收。(当然一般会把回收动作切割到不同的时间段执行,防止影响性能)

常用的两种垃圾回收规则是:

  • 标记清除
  • 引用计数

Javascript引擎基础GC方案是(simple GC):mark and sweep(标记清除),简单解释如下:

  1. 遍历所有可访问的对象。
  2. 回收已不可访问的对象。

譬如:(出自javascript高程)

当变量进入环境时,例如,在函数中声明一个变量,就将这个变量标记为“进入环境”。

从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到它们。

而当变量离开环境时,则将其标记为“离开环境”。

垃圾回收器在运行的时候会给存储在内存中的所有变量都加上标记(当然,可以使用任何标记方式)。

然后,它会去掉环境中的变量以及被环境中的变量引用的变量的标记(闭包,也就是说在环境中的以及相关引用的变量会被去除标记)。

而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。

最后,垃圾回收器完成内存清除工作,销毁那些带标记的值并回收它们所占用的内存空间。

关于引用计数,简单点理解:

跟踪记录每个值被引用的次数,当一个值被引用时,次数+1,减持时-1,下次垃圾回收器会回收次数为0的值的内存(当然了,容易出循环引用的bug)

GC的缺陷

和其他语言一样,javascript的GC策略也无法避免一个问题: GC时,停止响应其他操作

这是为了安全考虑。

而Javascript的GC在100ms甚至以上

对一般的应用还好,但对于JS游戏,动画对连贯性要求比较高的应用,就麻烦了。

这就是引擎需要优化的点: 避免GC造成的长时间停止响应。

GC优化策略

这里介绍常用到的:分代回收(Generation GC)

目的是通过区分“临时”与“持久”对象:

  • 多回收“临时对象”区(young generation
  • 少回收“持久对象”区(tenured generation
  • 减少每次需遍历的对象,从而减少每次GC的耗时。

像node v8引擎就是采用的分代回收(和java一样,作者是java虚拟机作者。)

更多可以参考:

V8 内存浅析

六 参考

七 总结

  • JS是解释型语言

  • 引擎对JS的处理: 词法分析 -> 得到token -> 语法分析 -> 得到AST -> 翻译器 -> 字节码(bytecode) -> 字节码解释器 -> 机器码

  • JS的预处理阶段 【分号不全,变量提升】

  • JS的执行阶段 【执行上下文, VO(变量对象)和AO(活动对象),作用域链】 在函数上下文中:VO === AO 在全局上下文中:VO === this === global

  • 垃圾回收机制GC【标记清除】

  • 每一个执行上下文,都有三个重要属性:VO 、 作用域链 、 this