Home > Archives > How JS Works学习笔记(1)

How JS Works学习笔记(1)

Publish:

最近学习了sessionstack上的系列文章How JS Works,对JavaScript的内部机制有了一些了解,因此写一个学习笔记来记录一下学到的知识。

先给出原文的链接:How JS Works

主要学习了三块知识:

1. JavaScript内部机制的综述,包括:引擎、调用栈、运行时等相关知识
2. V8引擎的工作原理 + 一些良好的代码优化习惯

学习JavaScript内部运行机制的目的在于我们希望通过更加深入的了解JS的内部机制,更好的利用语言和生态系统提供的所有技术,编写出更好的非阻塞应用程序。

下面就开始吧。

JavaScript内部机制:综述

JavaScript引擎

JavaScript引擎是一个执行JavaScript代码的程序或解释器,它有两种实现形式:

1.标准解释器,即直接执行高级语言
2.以某种形式将JavaScript代码编译成字节码的编译器

目前最流行的JavaScript引擎是Google的V8引擎,它被Chrome和Node.js使用。

V8引擎主要包括两个部分:

1.内存堆(Memory Heap): 内存分配发生的地方
2.调用栈(Call Stack): 代码执行所在的栈帧

内存堆由于比较复杂,所以放在下一部分阐述,这里主要阐述运行时和JS引擎中的调用栈。

运行时(Runtime)

Runtime这个词是和程序语言对应起来的,Runtime是指除去语言的标准库,运行一段语言程序时书写代码的机制。

JavaScript的运行时包含了浏览器提供的Web APIS(如:DOM、AJAX(HttpRequest)、Timeout等),事件循环相关的API以及回调函数的API等。

调用栈

JavaScript是单线程的,这意味着它只有一个调用栈。

调用栈记录了我们位于程序中的位置,调用栈中的一个条目被称为一个栈帧。

如果程序执行一个函数,就把该函数放在栈顶,如果从函数返回,就把它从栈顶弹出。

这是一个例子:

function multiply(x, y) {
    return x * y;
}

function printSquare(x) {
    var s = multiply(x, x);
    console.log(s);
}

printSquare(5);

执行这段代码时,调用栈的变化状况是这样的:

image

这也就意味着调用栈可以帮助我们追踪异常所在。

当达到最大调用栈大小时,会产生爆栈,也就是说,如果调用栈中的函数调用量超过了调用栈的实际大小,浏览器会抛出一个错误。

由于JavaScript是单线程的只有一个调用栈,并且,当调用栈里有函数在执行时,浏览器是被阻塞的,这意味着浏览器无法渲染,这会带来不好的用户体验,所以我们必须解决这个问题。

解决这个问题的方案是异步回调,这方面的内容会在第二篇学习笔记中阐述。

V8引擎的工作原理 + 一些良好的优化代码习惯

Google的V8引擎是开源的,用C++编写。

V8引擎为了提高JS的执行性能,没有采用上文阐述的两种方式来执行JS代码,而是通过JIT(即时)编译器,直接将JavaScript代码编译成机器码。

这里主要的区别是V8不会产生字节码或任何中间代码。

V8引擎的工作原理

V8引擎的内部是使用多线程的,它的多线程执行的任务主要是这样的:

1.主线程执行我们想让它干的活:获取代码,编译然后执行它
2.一个单独的编译线程(我们称其为优化线程),用于在主线程执行的同时优化代码
3.一个Profiler线程,用于让Runtime知道哪些方法花了大量时间,从而可以对其进行优化
4.几个线程用于GC

V8引擎的工作原理是这样的:

第一次执行JS代码时,主线程直接将解析的JS编译成机器码,快速开始执行。

代码运行一段时间后,Profiler线程已经收集了足够的数据来判断应该优化哪个方法。

优化线程对Profiler中分析的方法进行优化。

V8引擎的优化方法

1)内联

内联是用被调用的函数的函数体替换调用位置(调用函数所在的代码行)的过程。

2)隐藏类

大多数JS解释器都是用类似字典的结构(基于Hash函数)来存储对象的属性值。

由于JS的对象是动态的,所以也不可能像Java中一样固定对象布局,然而,用字典来查找内存中对象属性的位置是非常低效的。

因此V8使用了不同的方法的替代,这就是隐藏类。

隐藏类的工作机制类似于像Java这样的语言中使用的固定对象布局,但它是在运行时创建的。

原文中对隐藏类的构建的过程讲的很详细,所以我就不赘述了。

如果两个对象以相同的顺序初始化相同的动态属性,那么这两个对象可以共享一个隐藏类,这样就可以重用隐藏类而不需要重复构造了。

隐藏类使得JS中的对象属性也可以使用offset来进行访问,对属性查找的性能提升是显著的。

3)内联缓存

内联缓存的思想来自于一个观察的结果,即:对同一方法的重复调用往往发生在同一类型的对象上。

V8维护最近调用某一方法的对象类型的缓存,并根据该信息对将来可能调用该方法的对象类型做出假设,如果假设正确,就可以直接使用先前查找到的对象的隐藏类所存储的信息。

也就是说,连续两次对同一个隐藏类的同一方法进行成功调用后,V8就可以省掉查找对象隐藏类的过程,直接将属性的偏移地址加到对象指针本身上。

内联缓存也是为什么同一类型的对象共享隐藏类非常重要的原因。

4)当前栈替换(OSR)

在我们开始编译和优化一个明显要长期运行的方法之前,我们可能会运行它。

V8可以记忆刚刚执行的代码,所以它不会再用优化方法又执行一遍,而是将转换所有已有的上下文(栈、寄存器),以便我们直接执行程序的优化版本。

5)垃圾回收

V8使用的是标记、清扫这种传统的方法记性GC,不过为了控制成本,V8使用增量式标记:不是遍历整个堆,尝试标记每一个可能对象,而是遍历一部分堆,记录下停止位置,然后恢复正常执行,在下一次GC会从之前记录的位置开始。

良好的代码优化习惯

  1. 注意实例化对象属性的顺序

  2. 尽量不使用动态属性,防止隐藏类被修改

  3. 重复执行相同的方法执行效率更高

  4. 避免稀疏数组,稀疏数组是哈希表;避免预分配大数组,增量分配;避免删除数组中的元素

  5. 尽量使用在31位有符号数字的范围内的数字,避免JS对象装箱操作。

声明: 本文采用 BY-NC-SA 授权。转载请注明转自: How JS Works学习笔记(1) - 无火的余灰