图片 11

你不知道的JavaScript:对象

V8 Object 内存结构与属性访问

上世纪九十年代,随着网景浏览器的发行,JavaScript
首次进入人们的视线。之后随着 AJAX
的大规模应用与富客户端、单页应用时代的到来,JavaScript 在 Web
开发中占据了越来越重要的地位。在早期的 JavaScript
引擎中,性能越发成为了开发网页应用的瓶颈。而 V8
引擎设计的目标就是为了保证大型 JavaScript
应用的执行效率,在很多测试中可以明显发现
V8 的性能优于 JScript (Internet Explorer), SpiderMonkey (Firefox), 以及
JavaScriptCore(Safari). 根据 V8
的官方文档介绍,其主要是从属性访问、动态机器码生成以及高效的垃圾回收这三个方面着手性能优化。Obejct
当属 JavaScript
最重要的数据类型之一,本文我们对其内部结构进行详细阐述。其继承关系图如下所示:

图片 1

在 V8 中新分配的 JavaScript 对象结构如下所示:

[ class / map ] -> ... ; 指向内部类
[ properties  ] -> [empty array]
[ elements    ] -> [empty array] ; 数值类型名称的属性
[ reserved #1 ] -
[ reserved #2 ]  |
[ reserved #3 ]  }- in object properties,即预分配的内存空间
...............  |
[ reserved #N ] -/

在创建新的对象时,V8 会创建某个预分配的内存区域来存放所谓的 in-object
属性,预分配区域的大小由构造函数中的参数数目决定(this.field = expr)。当你打算向对象中添加某个新属性时,V8
首先会尝试放入所谓的 in-order 槽位中,当 in-object 槽位过载之后,V8
会尝试将新的属性添加到 out-of-object
属性列表。而属性名与属性下标的映射关系即存放在所谓隐藏类中,譬如{ a: 1, b: 2, c: 3, d: 4}对象的存储方式可能如下:

[ class       ] -> [a: in obj #1, b: in obj #2, c: out obj #1, d: out obj #2]
[ properties  ] -> [  3  ][  4  ] ; this is linear array
[ elements    ]    
[ 1           ]
[ 2           ]

随着属性数目的增加,V8 会转回到传统的字典模式/哈希表模式:

[ class       ] -> [ OBJECT IS IN DICTIONARY MODE ]
[ properties  ] -> [a: 1, b: 2, c: 3, d: 4, e: 5] ; this is classical hash table
[ elements    ]

概述

JavaScript引擎是一个执行JavaScript代码的程序或解释器。JavaScript引擎可以被实现为标准解释器,或者实现为以某种形式将JavaScript编译为字节码的即时编译器。

下面是实现了JavaScript引擎的一个热门项目列表:

  • V8 —
    开源,由Google开发,用C++编写的
  • Rhin o —
    由Mozilla基金所管理,开源,完全用Java开发
  • SpiderMonkey —第一个JavaScript引擎,最早用在Netscape
    Navigator上,现在用在Firefox上。
  • JavaScriptCore —
    开源,以Nitro销售,由苹果公司为Safari开发
  • KJS —KDE的引擎最初由Harri
    Porten开发,用于KDE项目的Konqueror浏览器
  • Chakra (JScript9) —
    Internet Explorer
  • Chakra (JavaScript) —
    Microsoft Edge
  • Nashorn —
    开源为OpenJDK的一部分,由Oracle的Java语言和工具组开发
  • JerryScript —
    是用于物联网的轻量级引擎

在前面我们看到过关于函数调用的位置不同this所绑定的对象也不同,但是对象是何物呢?

Reference

  • V8 Design
    Elements
  • A tour of V8: object
    representation
  • Demystifying v8 and JavaScript
    Performance
  • V8 Docs:Object Class
    Reference
  • How does V8 manage the memory of object
    instances?

创建V8引擎的由来

Google构建的V8引擎是开源的,用C++编写的。该引擎被用在Google
Chrome中。不过,与其他引擎不同的是,V8还被用作很受欢迎的Node.js的运行时。

图片 2

V8最初是设计用来提升Web浏览器中JavaScript执行的性能。为了获得速度,V8将JavaScript代码转换为更高效的机器码,而不是使用解释器。它通过实现像很多现代JavaScript引擎(比如SpiderMonkey或Rhino)所用的JIT(即时)编译器,从而将JavaScript代码编译成机器码。这里主要区别在于V8不会产生字节码或任何中间代码。

  • #### 语法

Property Name:属性名

作为动态语言,JavaScript 允许我们以非常灵活的方式来定义对象,譬如:

obj.prop
obj["prop"]

参照 JavaScript
定义规范中的描述,属性名恒为字符串,即使你使用了某个非字符串的名字,也会隐式地转化为字符串类型。譬如你创建的是个数组,以数值下标进行访问,然而
V8 还是将其转化为了字符串再进行索引,因此以下的方式就会获得相同的效果:

obj[1];    //
obj["1"];  // names for the same property
obj[1.0];  //

var o = { toString: function () { return "-1.5"; } };

obj[-1.5];  // also equivalent
obj[o];     // since o is converted to string

而 JavaScript 中的 Array
只是包含了额外的length属性的对象而已,length会返回当前最大下标加一的结果(此时字符串下标会被转化为数值类型计算):

var a = new Array();
a[100] = "foo";
a.length;  //101
a[undefined] = 'a';
a.length; //0

Function本质上也是对象,只不过length属性会返回参数的长度而已:

> a = ()=>{}
[Function: a]
> a.length
0
> a = (b)=>{}
[Function: a]
> a.length
1

V8曾经有两个编译器

在V8 的5.9版(今年早些时候发布)出现之前,V8引擎用了两个编译器:

  • full-codegen –
    一个简单而超快的编译器,可以生成简单而相对较慢的机器码。
  • Crankshaft –
    一个更复杂(即时)的优化的编译器,可以生成高度优化的代码。

V8引擎还在内部使用多个线程:

  • 主线程执行我们想让它干的活:获取代码,编译然后执行它
  • 还有一个单独的线程用于编译,这样在主线程继续执行的同时,单独的线程能同时在优化代码
  • 一个Profiler线程,用于让运行时知道哪些方法花了大量时间,这样Crankshaft就可以对它们进行优化
  • 几个线程用于处理垃圾收集器清扫

第一次执行JavaScript代码时,V8会利用 full-codegen 直接将解析的JavaScript翻译为机器码,而无需任何转换。这就让它能非常快地开始执行机器码。请注意,由于V8不会使用中间字节码表示,这样就无需解释器。

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

接下来, Crankshaft优化 从另一个线程中开始。它将JavaScript抽象语法树翻译为称为Hydrogen 的高级静态单赋值(SSA)表示,并尝试优化Hydrogen图。大多数优化都是在这一级完成的。

对象的定义可以通过声明形式和构造形式。

In-Object Properties & Fast Property Access:对象内属性与访问优化

作为动态类型语言,JavaScript
中的对象属性可以在运行时动态地增删,意味着整个对象的结构会频繁地改变。大部分
JavaScript 引擎倾向于使用字典类型的数据结构来存放对象属性( Object
Properties),每次进行属性访问的时候引擎都需要在内层中先动态定位属性对应的下标地址然后读取值。这种方式实现上比较容易,但是会导致较差的性能表现。其他的类似于
Java 与 Smalltalk
这样的静态语言中,成员变量在编译阶段即确定了其在内存中的固定偏移地址,进行属性访问的时候只需要单指令从内存中加载即可。而
V8
则利用动态创建隐藏内部类的方式动态地将属性的内存地址记录在对象内,从而提升整体的属性访问速度。总结而言,每当为某个对象添加新的属性时,V8
会自动修正其隐藏内部类。我们先通过某个实验来感受下隐藏类的存在:

var PROPERTIES = 10000000;
var o = {};

var start = +new Date;

for (var i = 0; i < PROPERTIES; i++) {
  o[i] = i;
}

console.log(+new Date - start);

function O(size) {
  for (var i = 0; i < size; i++) {
    this[i] = null;
  }
}

var o = new O(PROPERTIES);

var start = +new Date;

for (var i = 0; i < PROPERTIES; i++) {
  o[i] = i;
}

console.log(+new Date - start);

class OClass {

    constructor(size){
        for (var i = 0; i < size; i++) {
            this[i] = null;
        }
    }

}

var o = new OClass(PROPERTIES);

var start = +new Date;

for (var i = 0; i < PROPERTIES; i++) {
  o[i] = i;
}

console.log(+new Date - start);

该程序的执行结果如下:

// Babel 下结果
385
37
49
// Chrome 下结果
416
32
31

第一种实现中,每次为对象o设置新的属性时,V8
都会创建新的隐藏内部类(内部称为
Map)来存储新的内存地址以优化属性查找速度。而第二种实现时,我们在创建新的对象时即初始化了内部类,这样在赋值属性时
V8 以及能够高性能地定位这些属性。第三种实现则是用的 ES6 Class,在纯正的
V8
下性能最好。接下来我们具体阐述下隐藏类的工作原理,假设我们定义了描述点的函数:

function Point(x, y) {
  this.x = x;
  this.y = y;
}

当我们执行new Point(x,y)语句时,V8
会创建某个新的Point对象。创建的过程中,V8
首先会创建某个所谓C0的隐藏内部类,因为尚未为对象添加任何属性,此时隐藏类还是空的:

图片 3

接下来调用首个赋值语句this.x = x;为当前Point对象创建了新的属性x,此时
V8
会基于C0创建另一个隐藏类C1来替换C0,然后在C1中存放对象属性x的内存位置信息:

图片 4

这里从C0C1的变化称为转换(Transitions),当我们为同一个类型的对象添加新的属性时,并不是每次都会创建新的隐藏类,而是多个对象会共用某个符合转换条件的隐藏类。接下来继续执行this.y = y 这一条语句,会为Point对象创建新的属性。此时
V8 会进行以下步骤:

  • 基于C1创建另一个隐藏类C1,并且将关于属性y的位置信息写入到C2中。
  • 更新C1为其添加转换信息,即当为Point对象添加属性 y 时,应该转换到隐藏类 C2

图片 5

整个过程的伪代码描述如下:

<Point object is allocated>

  Class C0
    "x": TRANSITION to C1 at offset 0

this.x = x;

  Class C1
    "x": FIELD at offset 0
    "y": TRANSITION to C2 at offset 1

this.y = y;

  Map C2
    "x": FIELD at offset 0
    "y": FIELD at offset 1

内联

第一个优化是提前内联尽可能多的代码。内联是用被调用的函数的函数体替换调用位置(调用函数所在的代码行)的过程。这个简单的步骤让以下优化变得更有意义。

图片 6

隐藏类

JavaScript是一种基于原型的语言:它没有类,对象是用一种克隆过程创建的。JavaScript也是一种动态编程语言,就是说在对象实例化之后,可以随意给对象添加或删除属性。

大多数JavaScript解释器都使用类似字典的结构(基于哈希函数),将对象属性值的位置存储在内存中。这种结构使得在JavaScript中获取属性的值比在Java或C#这样的非动态编程语言中更昂贵。在Java中,所有对象属性都是由编译前的固定对象布局确定的,并且不能在运行时动态添加或删除(C#有动态类型,这是另一个话题了)。因此,属性的值(或指向这些属性的指针)可以在内存中存为连续缓冲区,每个缓冲区之间有固定偏移量。偏移量的长度可以很容易根据属性类型来确定。而在JavaScript中,这是不可能的,因为属性类型可能会在运行期间发生变化。

由于用字典来查找内存中对象属性的位置是非常低效的,所以V8使用了不同的方法来替代:隐藏类。隐藏类的工作机制类似于像Java这样的语言中使用的固定对象布局(类),只不过隐藏类是在运行时创建的。下面,我们来看看它们到底是什么样子:

function Point(x, y) {
    this.x = x;
    this.y = y;
}
 
var p1 = new Point(1, 2);

一旦 new Point(1, 2) 调用发生了,V8就会创建一个称为 C0 的隐藏类。

图片 7

因为还没有给 Point 定义属性,所以 C0 为空。

一旦执行了第一条语句 this.x = x (在 Point 函数中),V8就会创建一个基于
C0 的第二个隐藏类 C1 。 C1 描述了内存中的位置(相对于对象指针),属性 x
在这个位置可以找到。此时, x 存储在偏移地址0处,就是说,当将内存中的
point 对象作为连续缓冲器来查看时,第一个偏移地址就对应于属性 x
。V8也会用“类转换”来更新 C0 ,指出如果将一个属性 x
添加到点对象,那么隐藏类应该从 C0 切换到 C1 。下面的 point
对象的隐藏类现在是 C1 。

图片 8

每当向对象添加一个新属性时,旧的隐藏类就被用一个转换路径更新为新的隐藏类。隐藏类转换很重要,因为它们可以让隐藏类在以相同方式创建的对象之间共享。如果两个对象共享一个隐藏类,并且将相同的属性添加到这两个对象中,那么转换会确保两个对象都接收到相同的新隐藏类和它附带的所有优化过的代码。

当执行语句 this.y = y (同样是在 Point 函数内部, this.x = x
语句之后)时,会重复此过程。

这时,又创建一个名为 C2 的新隐藏类,类转换被添加到 C1 ,表示如果将属性 y
添加到 Point 对象(已包含属性 x ),那么隐藏类应更改为 C2 ,同时 point
对象的隐藏类被更新为 C2 。

图片 9

隐藏类转换取决于将属性添加到对象的顺序。看下面的代码片段:

function Point(x, y) {
    this.x = x;
    this.y = y;
}
 
var p1 = new Point(1, 2);
p1.a = 5;
p1.b = 6;
 
var p2 = new Point(3, 4);
p2.b = 7;
p2.a = 8;

现在,你可能会认为 p1 和 p2 会使用相同的隐藏类和转换。嗯,这是错的。对于
p1 ,首先是添加属性 a ,然后是属性 b 。不过,对于 p2 ,先是给 b
赋值,然后才是 a 。因此,由于转换路径不同, p1 和 p2
最终会有不同的隐藏类。在这种情况下,以相同的顺序初始化动态属性要更好,这样隐藏类才可以被重用。

//对象字面量形式
var obj = {};

//构造形式
var obj = new Object();
obj.name = "bob";

Reused Hidden Class:重复使用的隐藏类

我们在上文中提及,如果每次添加新的属性时都创建新的隐藏类无疑是极大的性能浪费,实际上当我们再次创建新的Point对象时,V8
并不会创建新的隐藏类而是使用已有的,过程描述如下:

  • 初始化新的Point对象,并将隐藏类指向C0
  • 添加x属性时,遵循隐藏类的转换原则指向到C1 ,
    并且根据C1指定的偏移地址写入x
  • 添加y属性时,遵循隐藏类的转换原则指向到C2,并且根据C2指定的偏移地址写入y

另外我们在上文以链表的方式描述转换,实际上真实场景中 V8
会以树的结构来描述转换及其之间的关系,这样就能够用于类似于下面的属性一致而赋值顺序颠倒的场景:

function Point(x, y, reverse) {
  if (reverse) {
    this.x = x;
    this.y = y;
  } else {
    this.y = x;
    this.x = y;
  }
}

内联缓存

V8利用另一种称为内联缓存(inline
caching)的技术来优化动态类型语言。内联缓存来自于观察的结果:对同一方法的重复调用往往发生在同一类型的对象上。关于内联缓存的深入解释可以在 这里 找到。

下面我们打算谈谈内联缓存的一般概念(如果您没有时间阅读上面的深入解释的话)。

那么它是如何工作的呢?V8维护在最近的方法调用中作为参数传递的对象类型的缓存,并使用该信息对将来作为参数传递的对象类型做出假设。如果V8能够对传递给方法的对象类型做出一个很好的假设,那么它可以绕过算出如何访问对象的属性的过程,转而使用先前查找对象的隐藏类时所存储的信息。

那么隐藏类和内联缓存的概念是如何关联的呢?无论何时在特定对象上调用方法,V8引擎必须对该对象的隐藏类执行查找,以确定访问特定属性的偏移地址。在对同一个隐藏类的同一方法进行了两次成功的调用之后,V8就省掉了隐藏类查找,只将属性的偏移地址添加到对象指针本身上。对于所有将来对该方法的调用,V8引擎都会假定隐藏类没有改变,并使用先前查找中存储的偏移地址直接跳转到特定属性的内存地址。这会大大提高执行速度。

内联缓存也是为什么同一类型的对象共享隐藏类非常重要的原因。如果您创建相同类型的两个对象,但是用的是不同的隐藏类(如前面的示例),那么V8将无法使用内联缓存,因为即使两个对象的类型相同,但是它们的对应隐藏类也会为其属性分配不同的偏移地址。

图片 10

两个对象基本相同,但是“a”和“b”属性是按照不同的顺序创建的。

构造形式和文字形式生成的对象是一样的。唯一的区别是,在字面量形式中我们可以任意添加多个属性,但是在构造形式中我们必须逐个添加属性。

Methods & Prototypes:方法与原型

JavaScript 中并没有类的概念(语法糖除外),因此对于方法的调用处理会难于
C++ 或者
Java。下面这个例子中,distance方法可以被看做Point的普通属性之一,不过其并非原始类型的数据,而是指向了另一个函数:

function Point(x, y) {
  this.x = x;
  this.y = y;
  this.distance = PointDistance;
}

function PointDistance(p) {
  var dx = this.x - p.x;
  var dy = this.y - p.y;
  return Math.sqrt(dx*dx + dy*dy);
}

如果我们像上文介绍的普通的 in-object
域一样来处理distance属性,那么无疑会带来较大的内存浪费,毕竟每个对象都要存放一段外部函数引用(Reference
的内存占用往往大于原始类型)。C++
中则是以指向多个虚函数的虚函数表(V-Tables)解决这个问题。每个包含虚函数的类的实例都会指向这个虚函数表,当调用某个虚函数时,程序会自动从虚函数表中加载该函数的地址信息然后转向到该地址调用。V8
中我们已经使用了隐藏类这一共享数据结构,因此可以很方便地改造下就可以。我们引入了所谓
Constant Functions 的概念,某个 Constant Function
即代表了对象中仅包含某个名字,而具体的属性值存放在描述符本身的概念:

<Point object is allocated>

  Class C0
    "x": TRANSITION to C1 at offset 0

this.x = x;

  Class C1
    "x": FIELD at offset 0
    "y": TRANSITION to C2 at offset 1

this.y = y;

  Class C2
    "x": FIELD at offset 0
    "y": FIELD at offset 1
    "distance": TRANSITION to C3 <PointDistance>

this.distance = PointDistance;

  Class C3
    "x": FIELD at offset 0
    "y": FIELD at offset 1
    "distance": CONSTANT_FUNCTION <PointDistance>

注意,在这里如果我们将PointDistance重定义指向了其他函数,那么这个转换也会自动失效,V8
会创建新的隐藏类。另一种解决这个问题的方法就是使用原型,每个构造函数都会有所谓的Prototype属性,该属性会自动成为对象的原型链上的一环,上面的例子可以改写为以下方式:

function Point(x, y) {
  this.x = x;
  this.y = y;
}

Point.prototype.distance = function(p) {
  var dx = this.x - p.x;
  var dy = this.y - p.y;
  return Math.sqrt(dx*dx + dy*dy);
}

...
var u = new Point(1, 2);
var v = new Point(3, 4);
var d = u.distance(v);

V8 同样会把原型链上的方法在隐藏类中映射为 Constant Function
描述符,而调用原型方法往往会比调用自身方法慢一点,毕竟引擎不仅要去扫描自身的隐藏类,还要去扫描原型链上对象的隐藏类才能得知真正的函数调用地址。不过这个不会对于代码的性能造成明显的影响,因此写代码的时候也不必小心翼翼的避免这个。

编译到机器码

一旦Hydrogen图被优化,Crankshaft将其降低到一个称为Lithium的较低级别表示。大多数Lithium实现都是针对架构的。寄存器分配发生在这一级。

最后,Lithium被编译成机器码。然后其他事情,也就是OSR(当前栈替换,on-stack
replacement),发生了。在我们开始编译和优化一个明显要长期运行的方法之前,我们可能会运行它。V8不会蠢到忘记它刚刚慢慢执行的代码,所以它不会再用优化版本又执行一遍,而是将转换所有已有的上下文(栈、寄存器),以便我们可以在执行过程中间就切换到优化版本。这是一个非常复杂的任务,请记住,除了其他优化之外,V8最开始时已经内联了代码。V8并非唯一能够做到这一点的引擎。

有一种称为去优化的保护措施,会作出相反的转换,并恢复为非优化代码,以防引擎的假设不再成立。

  • #### 类型

Dictionary Mode

对于复杂属性的对象,V8 会使用所谓的字典模式(Dictionary
Mode)来存储对象,也就是使用哈希表来存放键值信息,这种方式存储开销会小于上文提到的包含了隐藏类的方式,不过查询速度会远小于前者。初始状态下,哈希表中的所有的键与值都被设置为了undefined,当插入新的数据时,计算得出的键名的哈希值的低位会被当做初始的存储索引地址。如果此地址已经被占用了,V8
会尝试向下一个地址进行插入,直到插入成功,伪代码表述如下:

// 插入
insert(table, key, value):
  table = ensureCapacity(table, length(table) + 1)
  code = hash(key)
  n = capacity(table)
  index = code (mod n)
  while getKey(table, index) is not undefined:
    index += 1 (mod n)
  set(table, index, key, value)

//查找
lookup(table, key):
  code = hash(key)
  n = capacity(table)
  index = code (mod n)
  k = getKey(table, index)
  while k is not null or undefined
        and k != key: 
    index += 1 (mod n)
    k = getKey(table, index)
  if k == key:
    return getValue(table, index)
  else:
    return undefined

尽管计算键名哈希值与比较的速度会比较快,但是每次读写属性的时候都进行这么多步骤无疑会大大拉低速度,因此
V8 尽可能地会避免使用这种存储方式。

垃圾回收

对于垃圾回收来说,V8采用的是标记、清扫这种传统分代方式来清除旧一代。标记阶段应该停止执行JavaScript。为了控制GC成本,并使执行更加稳定,V8使用增量式标记:不是遍历整个堆,尝试标记每一个可能的对象,而是只遍历一部分堆,然后恢复正常执行。下一个GC停止会从之前的堆遍历停止的地方继续。这就允许在正常执行期间有非常短的暂停。如前所述,清扫阶段是由单独的线程处理。

在JavaScript中主要有6中数据类型:

Fast Elements:数值下标的属性

V8
中将属性名为非负整数(0、1、2……)的属性称为Element,每个对象都有一个指向Element数组的指针,其存放和其他属性是分开的。注意,隐藏类中并不包含
Element 的描述符,但可能包含其它有着不同 Element
类型的同一种隐藏类的转换描述符。大多数情况下,对象都会有 Fast
Element,也就是说这些 Element 以连续数组的形式存放。有三种不同的 Fast
Element:

  • Fast small integers
  • Fast doubles
  • Fast values

根据标准,JavaScript 中的所有数字都理应以64位浮点数形式出现。因此
V8
尽可能以31位带符号整数来表达数字(最低位总是0,这有助于垃圾回收器区分数字和指针)。因此含有Fast
small integers类型的对象,其 Element
类型只会包含这样的数字。如果需要存储小数、大整数或其他特殊值,如-0,则需要将数组提升为
Fast
doubles。于是这引入了潜在的昂贵的复制-转换操作,但通常不会频繁发生。Fast
doubles
仍然是很快的,因为所有的数字都是无封箱存储的。但如果我们要存储的是其他类型,比如字符串或者对象,则必须将其提升为普通的
Fast Element 数组。

JavaScript
不提供任何确定存储元素多少的办法。你可能会说像这样的办法,new Array(100),但实际上这仅仅针对Array构造函数有用。如果你将值存在一个不存在的下标上,V8会重新开辟更大的内存,将原有元素复制到新内存。V8
可以处理带空洞的数组,也就是只有某些下标是存有元素,而期间的下标都是空的。其内部会安插特殊的哨兵值,因此试图访问未赋值的下标,会得到undefined。当然,Fast
Element 也有其限制。如果你在远远超过当前数组大小的下标赋值,V8
会将数组转换为字典模式,将值以哈希表的形式存储。这对于稀疏数组来说很有用,但性能上肯定打了折扣,无论是从转换这一过程来说,还是从之后的访问来说。如果你需要复制整个数组,不要逆向复制(索引从高到低),因为这几乎必然触发字典模式。

    // 这会大大降低大数组的性能
    function copy(a) {
        var b = new Array();
        for (var i = a.length - 1; i >= 0; i--)
            b[i] = a[i];
        return b;
    }

由于普通的属性和数字式属性分开存放,即使数组退化为字典模式,也不会影响到其他属性的访问速度(反之亦然)。

Ignition 和 TurboFan

随着2017年早些时候版本5.9的发布,V8引入了一个新的执行管道。这个新的管道在真实的JavaScript应用程序中实现了更大的性能提升和显著的内存节省。

这个新的执行管道建立在V8的解释器 Ignition 和V8的最新优化编译器 TurboFan 之上。

您可以在 这里 查看V8团队关于这个主题的博文。

自从5.9版本发布以来,V8不再用full-codeget 和
Crankshaft(自2010年以来V8所用的技术)执行JavaScript,因为V8团队一直在努力跟上新的JavaScript语言特性,而这些特性需要优化。

这意味着V8整体下一步会有更简单和更易维护的架构。

图片 11

在Web和Node.js基准测试上的提升

这些提升仅仅是开始。新的Ignition和TurboFan管道为进一步优化铺平了道路,这将在未来几年内促进JavaScript性能提升,并缩小V8在Chrome和Node.js中所占比重。

最后,这里有一些关于如何编写良好优化、更佳的JavaScript的诀窍。当然,从上面的内容不难得到这些诀窍,不过,为了方便起见,这里还是给出一个摘要:

如何编写优化的JavaScript

  1. 对象属性的顺序 :始终以相同的顺序实例化对象属性,以便可以共享隐藏类和随后优化的代码。
  2. 动态属性 :在实例化后向对象添加属性会强制修改隐藏类,减慢为之前的隐藏类优化了的方法。所以应该在构造函数中指定对象的所有属性。
  3. 方法 :重复执行相同方法的代码将比只执行一次的代码(由于内联缓存)运行得快。
  4. 数组 :避免键不是增量数字的稀疏数组。元素不全的稀疏数组是一个 哈希表, 而访问这种数组中的元素更昂贵。另外,尽量避免预分配大数组。最好随着发展而增长。最后,不要删除数组中的元素。它会让键变得稀疏。
  5. 标记值 :V8用32位表示对象和数字。它用一位来判断是对象(flag =
    1)还是整数(flag=0)(这个整数称为SMI(SMall
    Integer,小整数),因为它是31位)。然后,如果一个数值大于31位,V8将会对数字装箱,将其转化为
    double,并创建一个新对象将该数字放在里面。所以要尽可能使用31位有符号数字,从而避免昂贵的转换为JS对象的装箱操作。

我们在SessionStack中试图在编写高度优化的JavaScript代码中遵循这些最佳实践。原因是一旦将SessionStack集成到产品web应用程序中,它就开始记录所有内容:所有DOM更改、用户交互、JavaScript异常、栈跟踪、失败的网络请求和调试消息。用SessionStack,您可以将Web应用中的问题重放为视频,并查看用户发生的一切。而所有这些都是在对您的web应用程序的性能不会产生影响的情况下发生的。

  • string
  • number
  • boolean
  • null
  • undefined
  • object

Object 代码声明

// https://v8docs.nodesource.com/node-7.2/d4/da0/v8_8h_source.html#l02660
class V8_EXPORT Object : public Value {
  public:
   V8_DEPRECATE_SOON("Use maybe version",
                     bool Set(Local<Value> key, Local<Value> value));
   V8_WARN_UNUSED_RESULT Maybe<bool> Set(Local<Context> context,
                                         Local<Value> key, Local<Value> value);

   V8_DEPRECATE_SOON("Use maybe version",
                     bool Set(uint32_t index, Local<Value> value));
   V8_WARN_UNUSED_RESULT Maybe<bool> Set(Local<Context> context, uint32_t index,
                                         Local<Value> value);

   // Implements CreateDataProperty (ECMA-262, 7.3.4).
   //
   // Defines a configurable, writable, enumerable property with the given value
   // on the object unless the property already exists and is not configurable
   // or the object is not extensible.
   //
   // Returns true on success.
   V8_WARN_UNUSED_RESULT Maybe<bool> CreateDataProperty(Local<Context> context,
                                                        Local<Name> key,
                                                        Local<Value> value);
   V8_WARN_UNUSED_RESULT Maybe<bool> CreateDataProperty(Local<Context> context,
                                                        uint32_t index,
                                                        Local<Value> value);

   // Implements DefineOwnProperty.
   //
   // In general, CreateDataProperty will be faster, however, does not allow
   // for specifying attributes.
   //
   // Returns true on success.
   V8_WARN_UNUSED_RESULT Maybe<bool> DefineOwnProperty(
       Local<Context> context, Local<Name> key, Local<Value> value,
       PropertyAttribute attributes = None);

   // Sets an own property on this object bypassing interceptors and
   // overriding accessors or read-only properties.
   //
   // Note that if the object has an interceptor the property will be set
   // locally, but since the interceptor takes precedence the local property
   // will only be returned if the interceptor doesn't return a value.
   //
   // Note also that this only works for named properties.
   V8_DEPRECATED("Use CreateDataProperty / DefineOwnProperty",
                 bool ForceSet(Local<Value> key, Local<Value> value,
                               PropertyAttribute attribs = None));
   V8_DEPRECATE_SOON("Use CreateDataProperty / DefineOwnProperty",
                     Maybe<bool> ForceSet(Local<Context> context,
                                          Local<Value> key, Local<Value> value,
                                          PropertyAttribute attribs = None));

   V8_DEPRECATE_SOON("Use maybe version", Local<Value> Get(Local<Value> key));
   V8_WARN_UNUSED_RESULT MaybeLocal<Value> Get(Local<Context> context,
                                               Local<Value> key);

   V8_DEPRECATE_SOON("Use maybe version", Local<Value> Get(uint32_t index));
   V8_WARN_UNUSED_RESULT MaybeLocal<Value> Get(Local<Context> context,
                                               uint32_t index);

   V8_DEPRECATED("Use maybe version",
                 PropertyAttribute GetPropertyAttributes(Local<Value> key));
   V8_WARN_UNUSED_RESULT Maybe<PropertyAttribute> GetPropertyAttributes(
       Local<Context> context, Local<Value> key);

   V8_DEPRECATED("Use maybe version",
                 Local<Value> GetOwnPropertyDescriptor(Local<String> key));
   V8_WARN_UNUSED_RESULT MaybeLocal<Value> GetOwnPropertyDescriptor(
       Local<Context> context, Local<String> key);

   V8_DEPRECATE_SOON("Use maybe version", bool Has(Local<Value> key));
   V8_WARN_UNUSED_RESULT Maybe<bool> Has(Local<Context> context,
                                         Local<Value> key);

   V8_DEPRECATE_SOON("Use maybe version", bool Delete(Local<Value> key));
   // TODO(dcarney): mark V8_WARN_UNUSED_RESULT
   Maybe<bool> Delete(Local<Context> context, Local<Value> key);

   V8_DEPRECATED("Use maybe version", bool Has(uint32_t index));
   V8_WARN_UNUSED_RESULT Maybe<bool> Has(Local<Context> context, uint32_t index);

   V8_DEPRECATED("Use maybe version", bool Delete(uint32_t index));
   // TODO(dcarney): mark V8_WARN_UNUSED_RESULT
   Maybe<bool> Delete(Local<Context> context, uint32_t index);

   V8_DEPRECATED("Use maybe version",
                 bool SetAccessor(Local<String> name,
                                  AccessorGetterCallback getter,
                                  AccessorSetterCallback setter = 0,
                                  Local<Value> data = Local<Value>(),
                                  AccessControl settings = DEFAULT,
                                  PropertyAttribute attribute = None));
   V8_DEPRECATED("Use maybe version",
                 bool SetAccessor(Local<Name> name,
                                  AccessorNameGetterCallback getter,
                                  AccessorNameSetterCallback setter = 0,
                                  Local<Value> data = Local<Value>(),
                                  AccessControl settings = DEFAULT,
                                  PropertyAttribute attribute = None));
   // TODO(dcarney): mark V8_WARN_UNUSED_RESULT
   Maybe<bool> SetAccessor(Local<Context> context, Local<Name> name,
                           AccessorNameGetterCallback getter,
                           AccessorNameSetterCallback setter = 0,
                           MaybeLocal<Value> data = MaybeLocal<Value>(),
                           AccessControl settings = DEFAULT,
                           PropertyAttribute attribute = None);

   void SetAccessorProperty(Local<Name> name, Local<Function> getter,
                            Local<Function> setter = Local<Function>(),
                            PropertyAttribute attribute = None,
                            AccessControl settings = DEFAULT);

   Maybe<bool> HasPrivate(Local<Context> context, Local<Private> key);
   Maybe<bool> SetPrivate(Local<Context> context, Local<Private> key,
                          Local<Value> value);
   Maybe<bool> DeletePrivate(Local<Context> context, Local<Private> key);
   MaybeLocal<Value> GetPrivate(Local<Context> context, Local<Private> key);

   V8_DEPRECATE_SOON("Use maybe version", Local<Array> GetPropertyNames());
   V8_WARN_UNUSED_RESULT MaybeLocal<Array> GetPropertyNames(
       Local<Context> context);
   V8_WARN_UNUSED_RESULT MaybeLocal<Array> GetPropertyNames(
       Local<Context> context, KeyCollectionMode mode,
       PropertyFilter property_filter, IndexFilter index_filter);

   V8_DEPRECATE_SOON("Use maybe version", Local<Array> GetOwnPropertyNames());
   V8_WARN_UNUSED_RESULT MaybeLocal<Array> GetOwnPropertyNames(
       Local<Context> context);

   V8_WARN_UNUSED_RESULT MaybeLocal<Array> GetOwnPropertyNames(
       Local<Context> context, PropertyFilter filter);

   Local<Value> GetPrototype();

   V8_DEPRECATED("Use maybe version", bool SetPrototype(Local<Value> prototype));
   V8_WARN_UNUSED_RESULT Maybe<bool> SetPrototype(Local<Context> context,
                                                  Local<Value> prototype);

   Local<Object> FindInstanceInPrototypeChain(Local<FunctionTemplate> tmpl);

   V8_DEPRECATED("Use maybe version", Local<String> ObjectProtoToString());
   V8_WARN_UNUSED_RESULT MaybeLocal<String> ObjectProtoToString(
       Local<Context> context);

   Local<String> GetConstructorName();

   Maybe<bool> SetIntegrityLevel(Local<Context> context, IntegrityLevel level);

   int InternalFieldCount();

   V8_INLINE static int InternalFieldCount(
       const PersistentBase<Object>& object) {
     return object.val_->InternalFieldCount();
   }

   V8_INLINE Local<Value> GetInternalField(int index);

   void SetInternalField(int index, Local<Value> value);

   V8_INLINE void* GetAlignedPointerFromInternalField(int index);

   V8_INLINE static void* GetAlignedPointerFromInternalField(
       const PersistentBase<Object>& object, int index) {
     return object.val_->GetAlignedPointerFromInternalField(index);
   }

   void SetAlignedPointerInInternalField(int index, void* value);
   void SetAlignedPointerInInternalFields(int argc, int indices[],
                                          void* values[]);

   // Testers for local properties.
   V8_DEPRECATED("Use maybe version", bool HasOwnProperty(Local<String> key));
   V8_WARN_UNUSED_RESULT Maybe<bool> HasOwnProperty(Local<Context> context,
                                                    Local<Name> key);
   V8_WARN_UNUSED_RESULT Maybe<bool> HasOwnProperty(Local<Context> context,
                                                    uint32_t index);
   V8_DEPRECATE_SOON("Use maybe version",
                     bool HasRealNamedProperty(Local<String> key));
   V8_WARN_UNUSED_RESULT Maybe<bool> HasRealNamedProperty(Local<Context> context,
                                                          Local<Name> key);
   V8_DEPRECATE_SOON("Use maybe version",
                     bool HasRealIndexedProperty(uint32_t index));
   V8_WARN_UNUSED_RESULT Maybe<bool> HasRealIndexedProperty(
       Local<Context> context, uint32_t index);
   V8_DEPRECATE_SOON("Use maybe version",
                     bool HasRealNamedCallbackProperty(Local<String> key));
   V8_WARN_UNUSED_RESULT Maybe<bool> HasRealNamedCallbackProperty(
       Local<Context> context, Local<Name> key);

   V8_DEPRECATED(
       "Use maybe version",
       Local<Value> GetRealNamedPropertyInPrototypeChain(Local<String> key));
   V8_WARN_UNUSED_RESULT MaybeLocal<Value> GetRealNamedPropertyInPrototypeChain(
       Local<Context> context, Local<Name> key);

   V8_DEPRECATED(
       "Use maybe version",
       Maybe<PropertyAttribute> GetRealNamedPropertyAttributesInPrototypeChain(
           Local<String> key));
   V8_WARN_UNUSED_RESULT Maybe<PropertyAttribute>
   GetRealNamedPropertyAttributesInPrototypeChain(Local<Context> context,
                                                  Local<Name> key);

   V8_DEPRECATED("Use maybe version",
                 Local<Value> GetRealNamedProperty(Local<String> key));
   V8_WARN_UNUSED_RESULT MaybeLocal<Value> GetRealNamedProperty(
       Local<Context> context, Local<Name> key);

   V8_DEPRECATED("Use maybe version",
                 Maybe<PropertyAttribute> GetRealNamedPropertyAttributes(
                     Local<String> key));
   V8_WARN_UNUSED_RESULT Maybe<PropertyAttribute> GetRealNamedPropertyAttributes(
       Local<Context> context, Local<Name> key);

   bool HasNamedLookupInterceptor();

   bool HasIndexedLookupInterceptor();

   int GetIdentityHash();

   // TODO(dcarney): take an isolate and optionally bail out?
   Local<Object> Clone();

   Local<Context> CreationContext();

   bool IsCallable();

   bool IsConstructor();

   V8_DEPRECATED("Use maybe version",
                 Local<Value> CallAsFunction(Local<Value> recv, int argc,
                                             Local<Value> argv[]));
   V8_WARN_UNUSED_RESULT MaybeLocal<Value> CallAsFunction(Local<Context> context,
                                                          Local<Value> recv,
                                                          int argc,
                                                          Local<Value> argv[]);

   V8_DEPRECATED("Use maybe version",
                 Local<Value> CallAsConstructor(int argc, Local<Value> argv[]));
   V8_WARN_UNUSED_RESULT MaybeLocal<Value> CallAsConstructor(
       Local<Context> context, int argc, Local<Value> argv[]);

   V8_DEPRECATE_SOON("Keep track of isolate correctly", Isolate* GetIsolate());

   static Local<Object> New(Isolate* isolate);

   V8_INLINE static Object* Cast(Value* obj);

  private:
   Object();
   static void CheckCast(Value* obj);
   Local<Value> SlowGetInternalField(int index);
   void* SlowGetAlignedPointerFromInternalField(int index);
 };

注意,简单的基本类型并不是对象(string,number,boolean,null,undefined)。null有时会被当做是一种对象类型。对null进行typeof
null时会返回字符串”object”。实际上null的本身是基本类型。

  • #### 内置对象

JavaScript中还有一些对象的子类型,通常被称为内置对象。

  • String
  • Number
  • Boolean
  • Object
  • Function
  • Array
  • Date
  • RegExp
  • Math
  • Error

这些内置对象可以理解像是其他语言中的type或者class。但是在JavaScript中,他们只是一些内置函数,这些内置函数可以当做构造函数来使用。

  • #### 内容

对象的内容是由一些存储在特定命名位置的,任意类型的值组成的,我们称之为属性。

在引擎内部,对象的值得存储方式是多样的,一般并不会存在对象容器内部。存储在对象容器内部的是这些属性的名称,它们类似于指针(引用)一样,指向这些值真正的存储位置。

访问对象属性的值有两种方式,“.”和“[]”,其访问并没有什么区别。

var obj = {a: 2};
obj.a;  //称为属性访问
obj['a']; //称为键访问
  • #### 数组

在数组中也支持[]访问,不过数组有一套更加结构化的值存储机制。数组期望的是数值下标,也就是说值存储的位置是非负整数。

数组也是对象,所以每个下标都是整数,你任然可以给数组添加属性。

var arr = [1, 2, 3];
arr.baz = 'baz';
arr.length; //3
arr.baz; //'baz'

可以看到给数组添加了属性,但是并未对数组的长度进行改变。单数如果你给数组添加一个“数字”属性,那么它会将整个“数字”属性转换为数组的下标。

var arr = [1, 2, 3];
aa['3'] = 4;
arr.length = 4;
arr[3]; // 4
  • ##### 属性描述符

在ES5之前,JavaScript没有提供直接检测属性特性的方法,但是在此之后,所有属性都具备了属性描述符。

var myObj = {
  a: 2
}
myObj.getOwnPropertyDescriptor(myObj, 'a');
// {
//   configurable: true,
//   enumerable: true,
//   writable: true,
//   value: 2
// }

如上,该对象对应的属性描述符包含configurable,enumerable,writable,value四个特性。分别代表可配置,可枚举,可写,数据值。

在创建普通属性时属性描述符会使用默认值,我们还可以通过Object.defineProperty(…)来添加一个新属性或者修改一个已有的属性(如果它的configuable为true)。

var obj = {

}

Object.defineProperty(obj, 'a', {
  configurable: true,
  enumerable: true,
  writable: true,
  value: 4
});

obj.a; //4

一. Writable

writable决定是否可以修改属性的值。

var myObject = {

}

Object.defineProperty(myObject, 'a', {
  value: 2,
  writable: false,  //不可写
  enumerable: true,
  configurable: true
});

myObject.a; //2
myObject.a = 4;
myObject.a; //2

如上,我们将writable设置为false,所以后来我们对该属性值的修改失败,如果在严格模式下,这种方法会出错。

二. Configurable

只要属性是可以配置的,就可以使用defineProperty(…)方法来修改属性描述符。

var myObj = {
  a: 2
};

myObj.a = 3;
myObj.a; //3

Object.defineProperty(myObj, 'a', {
  value: 4,
  writable: true,
  configurable: false, //不可配置
  enumerable: true
});

myObj.a; //4
myObj.a = 5;
myObj.a; //5

myObj.defineProperty(myObj, 'a', {
  value: 6,
  writable: true,
  configurable: true,
  enumerable: true
}); //TypeError

如上操作,最后使用defineProperty(…)会产生一个报错,不管是否处于严格模式下,尝试修改一个不可配置的描述符属性都会出错。注意,将configurable设置为FALSE是一个单向操作。(configurable为false的情况下,我们还是可以将writable设置为false,但是不可以将writable从false设置为true。)

除了无法设置该属性,configuable为false的情况下,也是禁止删除掉该属性。

三. Enumerable

该属性是用来控制属性时否会出现在对象的属性枚举中,比如for-in循环,如果将enumerable设置为false,那么这个属性就不会出现在枚举当中。

四. [[GET]]

当访问某个对象的属性的时候,例如查看obj对象的a属性,看起来只是查找到名字为a的属性。其实是在obj上实现了[[Get]]操作。对象默认的内置[[Get]]操作首先会在对象中查找是否有名称相同的属性。如果找到则返回该值。

然而如果没有找到,按照[[Get]]的算法的定义,遍历可能存在的[[Prototype]]链,就是原型链。如果最终都没有找到则会返回undefined。

五. [[Put]]

[[Put]]和[[Get]]是相对应的,但是[[Put]]被触发时,会取决于多种因素,包括对象中是否已经存在该属性。

如果对象中已经存在该属性那么会进行如下操作:

  • 属性是否是访问描述符,如果是并且存在setter就调用setter。

  • 属性的描述符中writable是否为false。如果是则在非严格模式下失败,在严格模式下会提示异常。

  • 如果都不是则将该值设置为属性的值。

  • ##### Getter和Setter

对象默认的[[Put]]和[[Get]]操作分别可以控制属性值的设置和获取。

在ES5中可以通过getter和setter部分改写默认操作。getter是一个隐藏函数,会在获取属性值时调用,setter也是一个隐藏函数,会在设置属性值时调用。

var myobj = {
  //给a定义一个getter
  get a() {
    return 2;
  }
}

Object.defineProperty(myobj, 'b', {
  get: function() {
    return this.a*2
  },
  enumerable: true
});

myobj.a; //2
myobj.b; //4

不管是对象文字语法中的get
a(){…}还是defineProperty(…)中显示的定义,两者都会在对象中创建一个不包含值得属性,对于这个属性的访问会自动调用一个隐藏函数,隐藏函数的返回值会被当做属性访问的返回值。

  • ##### 存在性

我们如何判断某个属性是否在该对象中?

var myobj = {
  a: 2
};

('a' in myobj); //true
('b' in myobj); //false

myobj.hasOwnProperty('a'); //true
myobj.hasOwnProperty('b'); //false

如上,in操作符会检查属性是否在对象中以及该对象的[[Prototype]]原型链中,hasOwnProperty只会检查该属性是否在该对象中。

所有的普通对象都可以通过Object.prototype的委托来访问hasOwnProperty方法。但是例如Object.create(null)来创建的对象并不会委托到。

  • ##### 枚举

关于对象属性是否可枚举,这个关系到了对象属性的数据描述符中的enumerable属性。

var obj = {};

Object.defineProperty(obj, 'a', {
  enumerable: true,
  value: 2
});

Object.defineProperty(obj, 'b', {
  enumerable: false,
  value: 3
});

for( var i in obj){
  console.log(i, obj[i])
} // 'a' 2

如上,属性b并没有出现在for-in循环中,所以可枚举可以理解为可以出现在对象属性的遍历中。

我们还可以通过propertyIsEnumerable(…)检查,该方法判断属性是否属于该对象而不是出现在原型链中,并且enumerable为true。

在ES6中Object.keys()会返回一个数组,包括该对象所有可以枚举的属性的数组。Object.getOwnPropertyNames(…)会返回一个数组,包括所有的属性,无论是否可以枚举。

发表评论

电子邮件地址不会被公开。 必填项已用*标注