本文是我看 How JavaScript Work 系列文章的学习笔记。
英文原文地址:https://blog.sessionstack.com/tagged/tutorial
中文翻译版地址:https://github.com/Troland/how-javascript-works
JavaScript 引擎
Chrome 的 V8 引擎包括两个主要组件:
- 动态内存管理 —— 用于分配内存
- 调用栈 —— 执行 JavaScript 代码的堆栈结构
运行时
除了 JavaScript 引擎用于执行代码和保存变量外,还有浏览器环境提供的 Web API。Web API 中包含 DOM、AJAX、setTimeout 等方法。
调用栈
JavaScript 是一个单线程的编程语言,所以它只有一个调用栈,即一次只能做一件事情。
只有一个调用栈的特性会带来两个问题:
- 调用栈太多会抛出堆栈溢出错误。
function foo() {
foo();
}
foo();
-
调用栈中执行某个行为时间太久会假死,浏览器抛出不可控制的错误 —— 浏览器没有响应。
V8 引擎中的两个编译器
在 V8 5.9 版本之前的浏览器引擎中有两个编译器:
- full-codegen-一个简单且快速的编译器用来产出简单且运行相对缓慢的机器码。用于将 JavaScript 代码编译为机器码。
- Crankshaft-一个更复杂(即时)优化的编译器用来产生高效的代码。它用于收集数据并优化 JavaScript 中可以优化的部分。
而在 V8.5.9 版本之后的浏览器引擎不再使用上述两个编译器了。新的 V8 解释器 Ignition 和 V8 最新的优化编译器 TurboFan 让引擎的性能提升了不少。
在 V8 引擎中书写最优代码的 5 条小技巧
-
对象属性的顺序:总是以相同的顺序实例化对象属性,这样隐藏类及之后的优化代码都可以被共享。
当我们使用this.x = 100
的写法为对象添加属性时,会创建一个带有属性 x 的隐藏类。以此类推,再次添加this.y = 200
那么会基于带有 x 属性的隐藏类再创建带有 x 和 y 属性的隐藏类。如果多个对象属性的添加顺序是一样的,那么就可以共享隐藏类了。反之,两个不同顺序实例化的对象所创建的隐藏类是不同的。 - 动态属性:实例化之后为对象添加属性会致使为之前隐藏类优化的方法变慢。应该在对象构造函数中赋值对象的所有属性。
-
方法:重复执行相同方法的代码会比每次运行不同的方法的代码更快(多亏了内联缓存)。
内联缓存会将重复执行的方法地址缓存下来,再次执行是绕开查找方法行为直接到方法地址去执行方法。 - 数列:避免使用键不是递增数字的稀疏数列。稀疏数列中没有包含每个元素的数列称为一个哈希表。访问该数列中的元素会更加耗时。同样地,试着避免预先分配大型数组。最好是随着使用而递增。最后,不要删除数列中的元素。这会让键稀疏。
- 标记值:V8 用 32 位来表示对象和数字。它使用一位来辨别是对象(flag=1)或者是被称为 SMI(小整数) 的整数(flag=0),之所以是小整数是因为它是 31 位的。之后,如果一个数值比 31 位还要大,V8 将会装箱数字,把它转化为浮点数并且创建一个新的对象来存储这个数字。尽可能试着使用 31 位有符号数字来避免创建 JS 对象的耗时装箱操作。