在 V8 引擎中设置原型(prototypes)

在 V8 引擎中设置原型(prototypes)

原型(好比 func.prototype )是用来模拟类的实现。它们一般包含类的全部方法,它们的 __proto__ 就是“父类(superclass)”,它们设置好后就不会修改了。前端

原型在设置时的性能表现对于应用程序的启动时间相当重要,由于此时一般要创建起整个类的层次结构。android

转换对象形态(Transitioning object shapes)

对象被编码的主要方式是将隐藏类(描述)对象(内容)分隔开。当一个对象被实例化,和以前来自同一个构造函数的对象使用相同的初始化隐藏类。当属性被添加,对象从一个隐藏类切换到另外一个隐藏类,一般是在所谓的“转换树(transition tree)”中重复以前的转换。举个例子,好比咱们有如下的构造函数:ios

function C() {
  this.a = 1;
  this.b = 2;
}
复制代码

若是咱们实例化一个对象 var o = new C(),它首先会使用一个没有任何属性的初始化隐藏类 M0。当 a 被添加,咱们将从 M0 切换到一个新的隐藏类 M1,M1 描述属性 a。接着添加 b 的时候,咱们再切换到另外一个新的隐藏类来描述 abgit

若是咱们如今实例化第二个对象 var o2 = new C(),它将重复上面的转换。从 M0 开始,接着 M1,最后是 M2。ab 被添加完成。github

这样作有三个重要的好处:后端

  1. 尽管建立第一个对象的开销是很大的,而且要求咱们建立全部隐藏的类和转换,可是建立后续对象是很是快的。
  2. 结果对象比完整的字典要小。咱们只须要在对象中存储值,而不须要存储关于属性的信息(好比名称)。
  3. 咱们如今在内联缓存(inline cache)和优化代码时有一个对象形态可使用,之后访问相似形态的对象就能够在同一位置找,方便快捷。

这样有利于频繁建立类似形态的对象。一样的事情也发生在对象字面量中:{a:1, b:2} 内部也会有隐藏类 M0,M1 和 M2。缓存

网上有不少相关知识讲解,你们能够去看看 Lars Bak 的视频:bash

YouTube 视频见:V8: an open source JavaScript engine函数

原型(Prototypes)就像特别的雪花

不一样于常规构造函数实例化对象,原型是典型的不与其余对象分享形态的对象。这会带来三点变化:post

  1. 一般来说,没有对象能从缓存的转换(cached transitions)中受益,并且设置转换树(transition tree)的开销也是没有必要的。
  2. 建立全部转换隐藏类的内存开销是很大的。事实上,在改变这个以前,咱们一般会看到为了一个简单的原型就要用上一大堆的隐藏类。
  3. 从一个原型中加载实际上并不像在原型链中使用那么常见。若是咱们经过原型链从一个原型对象中加载,咱们将不会分发原型的隐藏类,以及须要用不一样的方法检查它是否有效。

为了优化原型,V8 对其形态的跟踪不一样于常规的转换对象,咱们不须要跟踪转换树(transition tree),而是将隐藏类调整为原型对象,让它保持高性能。举个例子,好比执行 delete object.property 会拖慢对象的性能,但若是是原型就不会出现这种状况。由于咱们老是会保持它们的可缓存性(有些问题咱们还在解决中)。

咱们也改变了原型的设置。原型包含了2个重要的阶段:设置使用。原型在设置阶段被编译成字典对象(dictionary objects)。在那个状态下存储原型的速度很是快的,并且不须要进入 C++ 的运行时(跨边界的花销是很是巨大的)。与建立一个转换隐藏类来初始化对象相比,这是一个巨大的进步,由于前者必须进入C++ 运行时才行。

任何对原型的直接访问,或者经过原型链访问原型,都会将它切换成使用状态,这样确保了全部访问今后时开始是快速的。当处于使用状态,即便你删除属性,在删除以后咱们也会快速的切换回来。

function Foo() {}
// 如今 proto 对象是"设置"模式。
var proto = Foo.prototype;
proto.method1 = function() { ... }
proto.method2 = function() { ... }

var o = new Foo();
// 切换 proto 到"使用"模式。
o.method1();

// 也会切换 proto 到"使用"模式。
proto.method1.call(o);
复制代码

它是原型吗?

为了用上上面说的优化方法,咱们须要知道一个对象是否真的会被做为原型使用。因为 JavaScript 的特性,咱们很难在编译阶段分析你的代码。出于这个缘由,咱们甚至没有尝试在对象建立过程当中肯定什么东西最终会成为原型(固然,之后可能会发生变化)。一旦咱们看到一个对象赋值给一个原型,咱们将对它进行标记。举个例子来说:

var o = {x:1};
func.prototype = o;
复制代码

一开始咱们也不知道 o 用做原型,直到赋值给 func.prototype。我像往常那样花费巨大的开销来建立对象。一旦像它那样被赋值,它就被标记成原型,进入设置阶段。当你使用它,就会进入使用阶段。

若是你像下面这样写,咱们会在属性添加前就知道 o 是一个原型。因而它将在添加属性前进入设置阶段,后面的代码执行就会快得多:

var o = {};
func.prototype = o;
o.x = 1;
复制代码

注意你也能够这样使用 var o = func.prototype,由于很显然 func.prototype 在建立时就知道它是一个原型。

怎样设置原型(prototypes)?

若是你用下面的方式设置原型,咱们在方法添加以前很容易就知道 func.prototype 就是一个原型:

// 若是默认的 Object.prototype 为 __proto__,则省略下面这行代码。
func.prototype = Object.create(…);
func.prototype.method1 = …
func.prototype.method2 = …
复制代码

虽然已经很不错了,但事实上咱们不得不为每一个方法都加载一次 func.prototype。尽管最近咱们正在进一步优化 func.prototype 的加载,但这种加载是没必要要的,性能和内存的使用将比直接访问本地变量访问更糟糕。

简而言之,理想的原型设置方法以下:

var proto = func.prototype = Object.create(…);
proto.method1 = …
proto.method2 = …
复制代码

感谢 Benedikt Meurer.


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索