澳门新葡亰网址下载深入类和继承内部原理+Babel和 TypeScript 之间转换

by admin on 2020年4月14日

5月4日,Facebook开源团队技术作者Joel Marcey在Hacker
News社区发布一则《Prepack帮助提高JavaScript代码的效率》,引起了社区的广泛讨论。

时间: 2019-01-25阅读: 457标签: babel概述

刚开始接触JavaScript的时候,很多童鞋只是盲目的去学习,而忽视了一些基础性的知识点,导致在去面试前端职位的时候,被问到一些技术点的时候,比如什么是JavaScript引擎,它怎么工作的,一脸懵逼,oo

官方宣称Prepack是一个优化JavaScript源代码的工具,实际上它是一个JavaScript的部分求值器(Partial
Evaluator),可在编译时执行原本在运行时的计算过程,并通过重写JavaScript代码来提高其执行效率。Prepack用简单的赋值序列来等效替换JavaScript代码包中的全局代码,从而消除了中间计算过程以及对象分配的操作。对于重初始化的代码,Prepack可以有效缓存JavaScript解析的结果,优化效果最佳。

在 JavaScript
中,没有基本类型,创建的所有东西都是对象。例如,创建一个新字符串:

作为一名 JavaScript 开发者,深入了解 JavaScript
引擎是如何工作的将有助于你了解自己所写代码的性能特征。接下来一起来学习下吧,全文共由五个部分组成:

以下五个概念可以帮助你更好地理解Prepack的运行机制:

const name = "SessionStack";

1.JavaScript 引擎工作流程:介绍 JavaScript
引擎的处理流水线,这一部分会涉及到解释器/编译器的内容,且会分点介绍不同引擎间的差别与共同点;

  • 抽象语法树(AST)Prepack运行在AST级别,使用Babel解析并生成JavaScript源代码。

  • 具体执行(Concrete
    Execution)
    Prepack的核心是一个JavaScript解释器,它与ECMAScript
    5几乎完全兼容,而且紧密地保持与ECMAScript
    2016语言规范的一致性,你可以将Prepack中的解释器视为完全参照JavaScript实现的。解释器能够跟踪并撤销包括所有对象Mutation在内的结果,从而能够进行推测优化(Speculative
    Optimization)。

  • 符号执行(Symbolic
    Execution)
    除了对具体值进行计算外,Prepack的解释器还可以操作受环境相互作用影响的抽象值。例如Date.now可以返回一个抽象值,你可以通过helper辅助函数(如__abstract())手动注入抽象值。Prepack会跟踪所有在抽象值上执行的操作,在遇到分支时,Prepack会执行并探索所有可能性。所以,Prepack实现了一套JavaScript的符号执行引擎。

  • 抽象释义(Abstract
    Interpretation)
    符号执行在遇到抽象值的分支时会分叉(fork),Prepack会在控制流合并点加入分歧执行(Diverged
    Execution)来实现抽象释义的形式。连接变量和堆属性可能会得到条件抽象值,Prepack会跟踪有关抽象值和型域(Type
    Domain)的信息。

  • 堆序列化(Heap
    Serialization)
    当全局代码返回,初始化阶段结束时,Prepack捕获最终的堆并按顺序排列堆栈,生成直观的JavaScript新代码,创建并链接初始化堆中可访问的所有对象。堆中的一些值可能是抽象值的计算结果,对于这些值,Prepack将生成原始程序完成计算所执行的代码。

接着在新创建的对象上调用不同的方法:

2.JavaScript 对象模型;

以下是官方提供的Prepack优化示例:

console.log(a.repeat(2)); // SessionStackSessionStackconsole.log(a.toLowerCase()); // sessionstack

3. 属性访问的优化:通过 Shapes、Transistion 链与树、ICs
等概念的穿插介绍引擎是如何优化获取对象属性的;

/* Hello World */
// Input
(function () {
  function hello() { return 'hello'; }
  function world() { return 'world'; }
  global.s = hello() + ' ' + world();
})();
// Output
(function () {
  s = "hello world";
})();

/* 消除抽象税 */
// Input
(function () {
  var self = this;
  ['A', 'B', 42].forEach(function(x) {
    var name = '_' + x.toString()[0].toLowerCase();
    var y = parseInt(x);
    self[name] = y ? y : x;
  });
})();
// Output
(function () {
  _a = "A";
  _b = "B";
  _4 = 42;
})();

/* 斐波那契 */
// Input
(function () {
  function fibonacci(x) {
    return x <= 1 ? x : fibonacci(x - 1) + fibonacci(x - 2);
  }
  global.x = fibonacci(23);
})();
// Output
(function () {
  x = 28657;
})();

/* 模块初始化 */
// Input
(function () {
  let moduleTable = {};
  function define(id, f) { moduleTable[id] = f; }
  function require(id) {
    let x = moduleTable[id];
    return x instanceof Function ? (moduleTable[id] = x()) : x;
  }
  global.require = require;
  define("one", function() { return 1; });
  define("two", function() { return require("one") + require("one"); });
  define("three", function() { return require("two") + require("one"); });
  define("four", function() { return require("three") + require("one"); });
})();
three = require("three");
// Output
(function () {
  function _2() {
    return 3 + 1;
  }
  var _1 = {
    one: 1,
    two: 2,
    three: 3,
    four: _2
  };
  function _0(id) {
    let x = _1[id];
    return x instanceof Function ? _1[id] = x() : x;
  }
  require = _0;
  three = 3;
})();

/* 环境相互作用与分支 */
// Input
(function(){
  function fib(x) { return x <= 1 ? x : fib(x - 1) + fib(x - 2); }
  let x = Date.now();
  if (x === 0) x = fib(10);
  global.result = x;
})();
// Output
(function () {
  var _0 = Date.now();
  if (typeof _0 !== "number") {
    throw new Error("Prepack model invariant violation");
  }
  result = _0 === 0 ? 55 : _0;
})();

与其他语言不同,在 JavaScript
中,字符串或数字的声明会自动创建一个封装值的对象,并提供不同的方法,甚至可以在基本类型上执行这些方法。

4. 高效存储数组;

Prepack团队对未来的规划如下:

另一个有趣的事实是,数组等复杂类型也是对象。如果检查数组实例的类型,你将看到它是一个对象。列表中每个元素的索引只是对象中的属性。当通过数组中的索引访问一个元素时,实际上是访问了数组对象的一个key值,并得到key对应的值。从数据的存储方式看时,这两个定义是相同的:

5.Take-aways:对全文内容做了一个小结,并给了两点建议。

短期

let names = [“SessionStack”];let names = { “0”: “SessionStack”, “length”: 1}

JavaScript 引擎工作流程

  • 稳定现有功能集,用于预优化(Prepack)React Native代码包

  • 集成React Native工具链

  • 根据React Native所用模块系统的假设来构建优化

因此,访问数组中的元素和对象的属性耗时是相同的。我(本文作者)通过多次的努力才发现这一点的。就是不久,我(本文作者)不得不对项目中的一段关键代码进行大规模优化。在尝试了所有简单的可选项之后,最后用数组替换了项目中使用的所有对象。理论上,访问数组中的元素比访问哈希映射中的键要快且对性能没有任何影响。在
JavaScript中,这两种操作都是作为访问哈希映射中的键来实现的,并且花费相同的时间。

JavaScript 引擎在解析源码后将其转换为抽象语法树,基于
AST,解释器便可以开始工作并产生字节码,此时引擎正在执行 JavaScript
代码。

中期

使用原型模拟类

为了使它执行得更快,可以将字节码与分析数据一起发给优化编译器。优化编译器根据已有的分析数据做出特定假设,然后生成高度优化的机器码。

  • 进一步优化序列化(Serialization),包括:消除不暴露特性(identity)的对象;消除未使用的导出属性,等等

  • 预优化每个函数、基本代码块、语句、表达式

  • 与ES6保持完全一致

  • 支持广泛的模块系统

  • 假设ES6支持某些功能,延迟完成或直接忽略Polyfill应用

  • 进一步实现Web和Node.js环境中的兼容性目标

  • 深入集成JavaScript虚拟机,改进堆反序列化过程,包括
    :暴露“对象懒初始化”的概念 –
    以一种JavaScript无感知的方式,在首次使用对象时对其进行初始化;通过专门的字节码提高普通对象创建的编码效率;将代码分为两个阶段:1)
    非环境依赖阶段,虚拟机可以安全地捕获并恢复生成的堆;2)环境依赖阶段,通过从环境中获得的值执行所有剩余的计算过程来拼凑具体的堆,等等

  • 总结循环和递归

一般的想到对象时,首先想到的是类。我们大都习惯于根据类及其之间的关系来构建应用程序。尽管
JavaScript
中的对象无处不在,但该语言并不使用传统的基于类的继承,相反,它依赖于原型来实现

如果在某点上一个假设被证明是不正确的,那么优化编译器会去优化并回退至解释器部分。

长期 – 利用Prepack作为一个平台

在 JavaScript
中,每个对象通过原型连接着另一个对象。当尝试访问对象上的属性或方法时,首先从对象本身开始查找,如果没有找到任何内容,则在对象的原型中继续查找。

澳门新葡亰网址下载 ,JavaScript 引擎中的解释器 / 编译器流程

  • JavaScript Playground –
    通过调整JavaScript引擎体验JavaScript特性,这些引擎由JavaScript所编写,托管在浏览器中;你可以把它想象成一个“Babel虚拟机”,实现了不能被编译的JavaScript新特性

  • 捉Bug – 发现异常崩溃、执行问题……

  • 效果分析,例如检测模块工厂函数可能的副作用或强制纯净注释

  • 类型分析

  • 信息流分析

  • 调用图推理,允许内联和代码索引

  • 自动测试生成,利用符号执行的特性与约束求解器(Constraint
    Solver)结合来计算执行不同执行路径的输入

  • 智能模糊(Smart Fuzzing)

  • JavaScript沙盒 – 以不可观察的方式有效地测试JavaScript代码

从一个简单的例子开始:

现在,让我们关注实际执行 JavaScript
代码的这部分流程,即代码被解释和优化的地方,并讨论其在主要的 JavaScript
引擎之间存在的一些差异。

目前Prepack仍处于早期开发阶段,尚未准备好在生产环境中使用,官方建议仅尝试使用,并欢迎提供反馈以帮助修复错误。

function Component(content) { this.content = content;}Component.prototype.render = function() { console.log(this.content);}

一般来说,所有 JavaSciript
引擎都有一个包含解释器和优化编译器的处理流程。其中,解释器可以快速生成未优化的字节码,而优化编译器会需要更长的时间,以便最终生成高度优化的机器码。

稿源:前端之巅

在Component的原型上添加render方法,因为希望Component的每个实例都能有render方法。Component任何实例调用此方法时,首先将在实例本身中执行查找,如果没有,接着从它的原型中执行查找。

这个通用流程几乎与在 Chrome 和 Node.js 中使用的 V8 引擎工作流程一致:

接着引入一个新的子类:

V8 中的解释器被称作
Ignition,它负责生成并执行字节码。当它运行字节码时会收集分析数据,而它之后可以被用于加快执行的速度。当一个函数变得
hot,例如它经常被调用,生成的字节码和分析数据则会被传给
TurboFan——我们的优化编译器,它会依据分析数据生成高度优化的机器码。

function InputField(value) { this.content = `input type="text" value="${value}" /`;}

SpiderMonkey,在 Firefox 和 SpiderNode 中使用的 Mozilla 的 JavaScript
引擎,则有一些不同的地方。它们有两个优化编译器。解释器将代码解释给
Baseline 编译器,该编译器可以生成部分优化的代码。
结合运行代码时收集的分析数据,IonMonkey 编译器可以生成高度优化的代码。
如果尝试优化失败,IonMonkey 将回退到 Baseline 阶段的代码。

如果想要InputField继承Component并能够调用它的render方法,就需要更改它的原型。当对子类的实例调用render方法时,不希望在它的空原型中查找,而应该从从Component上的原型查找:

Chakra,用于 Edge 和 Node-ChakraCore 两个项目的微软 JavaScript
引擎,也有类似两个优化编译器的设置。解释器将代码优化成 SimpleJIT——其中
JIT 代表 Just-In-Time 编译器——它可以生成部分优化的代码。
结合分析数据,FullJIT 可以生成更深入优化的代码。

InputField.prototype = Object.create(new Component());

JavaScriptCore,Apple 的 JavaScript 引擎,被用于 Safari 和 React Native
两个项目中,它通过三种不同的优化编译器使效果达到极致。低级解释器 LLInt
将代码解释后传递给 Baseline 编译器,而优化后的代码便传给了 DFG
编译器,结果最终传给了 FTL 编译器进行处理。

通过这种方式,就可以在Component的原型中找到render方法。为了实现继承,需要将InputField的原型连接到Component的实例上,大多数库都使用Object.setPrototypeOf方法来实现这一点。

为什么有些引擎会拥有更多的优化编译器呢?这完全是一些折衷的取舍。解释器可以快速生成字节码,但字节码通常不够高效。另一方面,优化编译器处理需要更长的时间,但最终会生成更高效的机器码。到底是快速获取可执行的代码,还是花费更多时间但最终以最佳性能运行代码,这其中包含一个平衡点。一些引擎选择添加具有不同耗时
/
效率特性的多个优化编译器,以更高的复杂性为代价来对这些折衷点进行更细粒度的控制。

然而,这不是唯一一件事要做的,每次继承一个类,需要:

我们刚刚强调了每个 JavaScript
引擎中解释器和优化编译器流程中的主要区别。除了这些差异之外,所有
JavaScript 引擎都有相同的架构:那就是拥有一个解析器和某种解释器 /
编译器流程。

将子类的原型指向父类的实例。在子类构造函数中调用的父构造函数,完成父构造函数中的初始化逻辑。

JavaScript 对象模型

如上所述,如果希望继承基类的的所有特性,那么每次都需要执行这个复杂的逻辑。当创建多个类时,将逻辑封装在可重用函数中是有意义的。这就是开发人员最初解决基于类继承的方法——通过使用不同的库来模拟它。

通过关注一些方面的具体实现,让我们来看看 JavaScript
引擎间还有哪些共同之处。

这些解决方案越来越流行,造成了 JS
中明显缺少了一些类型的现象。这就是为什么在 ECMAScript 2015
的第一个主要版本中引入了类,继承的新语法。

例如,JavaScript 引擎是如何实现 JavaScript
对象模型的,以及他们使用了哪些技巧来加快获取 JavaScript
对象属性的速度?事实证明,所有主要引擎在这一点上的实现都很相似。

类的转换

ECMAScript 规范基本上将所有对象定义为由字符串键值映射到 property 属性
的字典。

当 ES6 或 ECMAScript 2015 中的新特性被提出时,JavaScript
开发人员不能等待所有引擎和浏览器都开始支持它们。为实现浏览器能够支持新的特性一个好方法是通过转换
(Transpiling)
,它允许将 ECMAScript 2015
中编写的代码转换成任何浏览器都能理解的 JavaScript
代码,当然也包括使用基于类的继承编写类的转换功能。

除 [[Value]] 外,规范还定义了如下属性:

Babel

[[Writable]] 决定该属性是否可以被重新赋值;

最流行的 JavaScript 编译器之一就是
Babel,宏观来说,它分3个阶段运行代码:解析(parsing),转译(transforming),生成(generation),来看看它是如何转换的:

[[Enumerable]] 决定该属性是否出现在 for-in 循环中;

class Component { constructor(content) { this.content = content; } render() { console.log(this.content) }}const component = new Component('SessionStack');component.render();

[[Configurable]] 决定该属性是否可被删除。

以下是 Babel 转换后的样式:

[[双方括号]]
的符号表示看上去有些特别,但这正是规范定义不能直接暴露给 JavaScript
的属性的表示方法。在 JavaScript 中你仍然可以通过
Object.getOwnPropertyDescriptor API 获得指定对象的属性值:

var Component = function () { function Component(content) { _classCallCheck(this, Component); this.content = content; } _createClass(Component, [{ key: 'render', value: function render() { console.log(this.content); } }]); return Component;}();

JavaScript 就是这个定义对象的,那么数组呢?

如上所见,转换后的代码就可在任何浏览器执行了。 此外,还添加了一些功能,
这些是 Babel 标准库的一部分。

你可以将数组想象成一组特殊的对象。两者的一个区别便是数组会对数组索引进行特殊的处理。这里所指的数组索引是
ECMAScript 规范中的一个特殊术语。在 JavaScript
中,数组被限制最多只能拥有 2^32-1
项。数组索引是指该限制内的任何有效索引,即从 0 到 2^32-2 的任何整数。

_classCallCheck和_createClass作为函数包含在编译文件中。

另一个区别是数组还有一个充满魔力的 length 属性。

_classCallCheck函数的作用在于确保构造方法永远不会作为函数被调用,它会评估函数的上下文是否为Component对象的实例,以此确定是否需要抛出异常。_createClass用于处理创建对象属性,函数支持传入构造函数与需定义的键值对属性数组。函数判断传入的参数(普通方法/静态方法)是否为空对应到不同的处理流程上。

在这个例子中,array 在生成时长度单位为 2。接着我们向索引为 2
的位置分配了另一个元素,length 属性便自动更新。

为了探究继承的实现原理,分析继承的Component的InputField类。。

JavaScript
在定义数组的方式上和对象类似。例如,包括数组索引的所有键值都明确地表示为字符串。
数组中的第一个元素存储在键值为 ‘0’ 的位置下。

class InputField extends Component { constructor(value) { const content = `input type="text" value="${value}" /`; super(content); }}

‘length’ 属性恰好是另一个不可枚举且不可配置的属性。

使用 Babel 处理上述代码,得到如下代码:

一个元素一旦被添加到数组中,JavaScript 便会自动更新 ‘length’ 属性的
[[Value]] 属性值。

 var InputField = function (_Component) { _inherits(InputField, _Component); function InputField(value) { _classCallCheck(this, InputField); var content = 'input type="text" value="' + value + '" /'; return _possibleConstructorReturn(this, (InputField.__proto__ || Object.getPrototypeOf(InputField)).call(this, content)); } return InputField;}(Component);

一般来说,数组的行为与对象也非常相似。

在本例中, Babel 创建了_inherits函数帮助实现继承。

属性访问的优化

以 ES6 转 ES5 为例,具体过程:

让我们深入了解下 JavaScript 引擎是如何有效地应对对象相关操作的。

编写ES6代码babylon 进行解析解析得到 ASTplugin 用 babel-traverse 对 AST
树进行遍历转译得到新的 AST树用 babel-generator 通过 AST 树生成 ES5
代码Babel 中的抽象语法树

观察 JavaScript 程序,访问属性是最常见的一个操作。使得 JavaScript
引擎能够快速获取属性便至关重要。

AST 包含多个节点,且每个节点只有一个父节点。 在 Babel
中,每个形状树的节点包含可视化类型、位置、在树中的连接等信息。
有不同类型的节点,如string,numbers,null等,还有用于流控制(if)和循环(for,while)的语句节点。
并且还有一种特殊类型的节点用于类。它是基节点类的一个子节点,通过添加字段来扩展它,以存储对基类的引用和作为单独节点的类的主体。

Shapes

把下面的代码片段转换成一个抽象语法树:

在 JavaScript
程序中,多个对象具有相同的键值属性是非常常见的。这些对象都具有相同的形状。

class Component { constructor(content) { this.content = content; } render() { console.log(this.content) }}

访问具有相同形状对象的相同属性也很常见:

下面是以下代码片段的抽象语法树

考虑到这一点,JavaScript
引擎可以根据对象的形状来优化对象的属性获取。它是这么实现的。

Babel 的三个主要处理步骤分别是: 解析(parse),转换 (transform),生成
(generate)。

假设我们有一个具有属性 x 和 y
的对象,它使用我们前面讨论过的字典数据结构:它包含用字符串表示的键值,而它们指向各自的属性值。

解析

如果你访问某个属性,例如 object.y,JavaScript 引擎会在 JSObject
中查找键值 ‘y’,然后加载相应的属性值,最后返回 [[Value]]。

将代码解析成抽象语法树(AST),每个js引擎(比如Chrome浏览器中的V8引擎)都有自己的AST解析器,而Babel是通过
Babylon 实现的。在解析过程中有两个阶段: 词法分析 和 语法分析
,词法分析阶段把字符串形式的代码转换为 令牌
(tokens)流,令牌类似于AST中节点;而语法分析阶段则会把一个令牌流转换成
AST的形式,同时这个阶段会把令牌中的信息转换成AST的表述结构。

但这些属性值在内存中是如何存储的呢?我们是否应该将它们存储为 JSObject
的一部分?假设我们稍后会遇到更多同形状的对象,那么在 JSObject
自身存储包含属性名和属性值的完整字典便是很浪费的,因为对具有相同形状的所有对象我们都重复了一遍属性名称。
它太冗余且引入了不必要的内存使用。 作为优化,引擎将对象的 Shape
分开存储。

转换

Shape 包含除 [[Value]] 之外的所有属性名和其余特性。相反,Shape 包含
JSObject 内部值的偏移量,以便 JavaScript
引擎知道去哪查找具体值。每个具有相同形状的 JSObject 都指向这个 Shape
实例。 现在每个 JSObject 只需要存储对这个对象来说唯一的那些值。

在这个阶段,Babel接受得到AST并通过babel-traverse对其进行深度优先遍历,在此过程中对节点进行添加、更新及移除操作。这部分也是Babel插件介入工作的部分。

当我们有多个对象时,优势变得清晰可见。无论有多少个对象,只要它们具有相同的形状,我们只需要将它们的形状与键值属性信息存储一次!

生成

所有的 JavaScript 引擎都使用了形状作为优化,但称呼各有不同:

将经过转换的AST通过babel-generator再转换成js代码,过程就是深度优先遍历整个AST,然后构建可以表示转换后代码的字符串。

学术论文称它们为 Hidden Classes(容易与 JavaScript 中的类概念混淆)

在上面的示例中,首先生成两个MethodDefinition节点的代码,然后生成类主体节点的代码,最后生成类声明节点的代码。

V8 将它们称为 Maps(容易与 JavaScript 中的 Map 概念混淆)

使用 TypeScript 进行转换

Chakra 将它们称为 Types(容易与 JavaScript 中的动态类型和关键字 typeof
混淆)

另一个利用转换的流行框架是 TypeScript。它引入了一种用于编写 JavaScript
应用程序的新语法,该语法被转换为任何浏览器或引擎都可以执行的 EMCAScript
5。下面是用 Typescript 实现Component:

JavaScriptCore 称它们为 Structures

class Component { content: string; constructor(content: string) { this.content = content; } render() { console.log(this.content) }}

SpiderMonkey 称他们为 Shapes

转成抽象语法树如下:

本文中,我们会继续称它为 shapes。

Typescript 还支持继承:

Transition 链与树

class InputField extends Component { constructor(value: string) { const content = `input type="text" value="${value}" /`; super(content); }}

如果你有一个具有特定形状的对象,但你又向它添加了一个属性,此时会发生什么?
JavaScript 引擎是如何找到这个新形状的?

以下是转换结果:

在 JavaScript 引擎中,shapes 的表现形式被称作 transition
链。以下展示一个示例:

var InputField = /** @class */ (function (_super) { __extends(InputField, _super); function InputField(value) { var _this = this; var content = "input type="text" value="" + value + "" /"; _this = _super.call(this, content) || this; return _this; } return InputField;}(Component));

该对象在初始化时没有任何属性,因此它指向一个空的
shape。下一个语句为该对象添加值为 5 的属性 “x”,所以 JavaScript
引擎转向一个包含属性 “x” 的 Shape,并向 JSObject 的第一个偏移量为 0
处添加了一个值 5。 接下来一个语句添加了一个属性
‘y’,引擎便转向另一个包含 ‘x’ 和 ‘y’ 的 Shape,并将值 6 附加到
JSObject。

最终的结果还是 ECMAScript 5
代码,其中包含TypeScript库中的一些函数。封__extends中的逻辑与在第一节中讨论的逻辑相同。

我们甚至不需要为每个 Shape 存储完整的属性表。相反,每个 Shape
只需要知道它引入的新属性。 例如在此例中,我们不必在最后一个 Shape
中存储关于 ‘x’
的信息,因为它可以在更早的链上被找到。要做到这一点,每一个 Shape
都会与其之前的 Shape 相连:

随着 Babel 和 TypeScript 被广泛采用,标准类和基于类的继承成为了构造
JavaScript 应用程序的标准方式,这推动了在浏览器中引入对类的原生支持。

如果你在 JavaScript 代码中写到了 o.x,则 JavaScript 引擎会沿着
transition 链去查找属性 “x”,直到找到引入属性 “x”的 Shape。

类的原生支持

但是,如果不能只创建一个 transition
链呢?例如,如果你有两个空对象,并且你为每个对象都添加了一个不同的属性?

2014年,Chrome
引入了对类的原生支持,这允许在不需要任何库或转换器的情况下执行类声明语法。

在这种情况下我们便必须进行分支操作,此时我们最终会得到一个 transition 树
而不是 transition 链:

本地实现类的过程就是我们所说的语法糖。这只是一种奇特的语法,它可以编译成语言中已经支持的相同的原语。可以使用新的易于使用的类定义,但是它仍然会创建构造函数和分配原型。

在这里,我们创建一个空对象 a,然后为它添加一个属性 ‘x’。
我们最终得到一个包含单个值的 JSObject,以及两个 Shapes:空 Shape
和仅包含属性 x 的 Shape。

V8的支持

第二个例子也是从一个空对象 b 开始的,但之后被添加了一个不同的属性
‘y’。我们最终形成两个 shape 链,总共是三个 shape。

撯着,看看在 V8 中对 ECMAScript 2015
类的本机支持的工作原理。正如在前一篇文章中所讨论的,首先必须将新语法解析为有效的
JavaScript 代码并添加到 AST
中,因此,作为类定义的结果,一个具有ClassLiteral类型的新节点被添加到树中。

这是否意味着我们总是需要从空 shape 开始呢?
并不是。引擎对已包含属性的对象字面量会应用一些优化。比方说,我们要么从空对象字面量开始添加
x 属性,要么有一个已经包含属性 x 的对象字面量:

这个节点存储了一些信息。首先,它将构造函数作为一个单独的函数保存,还保存类属性的列表,这些属性包括
方法、getter、setter、公共字段或私有字段。该节点还存储对父类的引用,该类将继承父类,而父类将再次存储构造函数、属性列表和父类。

在第一个例子中,我们从空 shape 开始,然后转向包含 x 的
shape,这正如我们我们之前所见。

一旦这个新的类ClassLiteral被转换成代码,它又被转换成函数和原型。

在 object2 一例中,直接生成具有属性 x
的对象是有意义的,而不是从空对象开始然后进行 transition 连接。

原文:

包含属性 ‘x’ 的对象字面量从包含 ‘x’ 的 shape 开始,可以有效地跳过空的
shape。V8 和 SpiderMonkey 正是这么做的。这种优化缩短了 transition
链,并使得从字面量构造对象更加高效。

Benedikt 的博文 surprising polymorphism in React applications
讨论了这些微妙之处是如何影响实际性能的。

Inline Caches

Shapes 背后的主要动机是 Inline Caches 或 ICs 的概念。ICs 是促使
JavaScript 快速运行的关键因素!JavaScript 引擎利用 ICs
来记忆去哪里寻找对象属性的信息,以减少昂贵的查找次数。

这里有一个函数 getX,它接受一个对象并从中取出属性 x 的值:

如果我们在 JSC 中执行这个函数,它会生成如下字节码:

指令一 get_by_id 从第一个参数中加载属性 ‘x’ 值并将其存储到地址 loc0
中。 第二条指令返回我们存储到 loc0 中的内容。

JSC 还在 get_by_id 指令中嵌入了 Inline
Cache,它由两个未初始化的插槽组成。

现在让我们假设我们用对象 调用 getX
函数。正如我们所知,这个对象有一个包含属性 ‘x’ 的 Shape,该 Shape
存储了属性 x 的偏移量和其他特性。当你第一次执行该函数时,get_by_id
指令将查找属性 ‘x’,然后发现其值存储在偏移量 0 处。

嵌入到 get_by_id 指令中的 IC 存储该属性的 shape 和偏移量:

对于后续运行,IC 只需要对比
shape,如果它与以前相同,只需从记忆的偏移量处加载该属性值。具体来说,如果
JavaScript 引擎看到一个对象的 shape 之前被 IC
记录过,它则不再需要接触属性信息——而是完全可以跳过昂贵的属性信息查找。这比每次查找属性要快得多。

高效存储数组

对于数组来说,存储属性诸如数组索引等是非常常见的。这些属性的值被称为数组元素。存储每个数组中的每个数组元素的属性特性将是一种很浪费的存储方式。相反,由于数组索引默认属性是可写的、可枚举的并且可以配置的,JavaScript
引擎利用这一点,将数组元素与其他命名属性分开存储。

引擎存储了数组长度,并指向包含 offset 和 ‘length’ 特性属性的 Shape。

这与我们之前见过的类似……但数组值存储在哪里呢?

每个数组都有一个单独的 elements backing
store,其中包含所有数组索引的属性值。JavaScript
引擎不必为数组元素存储任何属性特性,因为它们通常都是可写的,可枚举的以及可配置的。

那么如果不是通常的情况呢?如果更改了数组元素的属性,该怎么办?

上面的代码片段定义了一个名为 ‘0’
的属性,但其特性被设置为了一个非默认值。

在这种边缘情况下,JavaScript 引擎会将全部的 elements backing store
表示为一个由数组下标映射到属性特性的字典。

即使只有一个数组元素具有非默认属性,整个数组的 backing store
处理也会进入这种缓慢而低效的模式。 避免在数组索引上使用
Object.defineProperty!
(我不知道为什么你会想这样做。这看上去似乎是一个奇怪的且毫无价值的事情。)

Take-aways

我们已经学习了 JavaScript 引擎是如何存储对象和数组的,以及 Shapes 和 IC
是如何优化针对它们的常见操作的。基于这些知识,我们确定了一些有助于提升性能的实用
JavaScript 编码技巧:

始终以相同的方式初始化对象,以确保它们不会走向不同的 shape 方向。

不要混淆数组元素的属性特性,以确保可以高效地存储和操作它们。

总结

每天一小步,成功一大步,坚持学习才是硬道理!

相关文章

发表评论

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

网站地图xml地图