这些代码中包含着相似于 _classCallCheck 和 _createClass这样的函数,而这些函数已经
在Babel和TypeScript的标准库中预先定义好了,而后进行处理)。顺便温习了Object.create这个方法, 好比有一个obj:{name:'是ho',f:function(){alert(1)}}javascript
var a = Object.create(obj)java
这时,a对象的原型就是这个objgit
以上等同于es6
var a github
a = {} //q是一个对象chrome
a.__proto__= objc#
这是 JavaScript 工做原理的第十五章。数组
现在使用类来组织各类软件工程代码是最经常使用的方法。本章将会探索实现 JavaScript 类的不一样方法及如何构建类继承。咱们将深刻理解原型继承及分析使用流行的类库模拟实现基于类继承的方法。接下来,将会介绍如何使用转换器为语言添加非原生支持的语法功能和如何在 Babel 和 TypeScript 中运用以支持 ECMAScript 2015 类。最后介绍几个 V8 原生支持实现类的例子。浏览器
JavaScript 没有原始类型且一切皆对象。好比,以下字符串:性能优化
const name = "SessionStack";
能够当即调用新建立对象上的不一样方法:
console.log(a.repeat(2)); // 输出 SessionStackSessionStack console.log(a.toLowerCase()); // 输出 sessionstack
JavaScript 和其它语言不同,声明一个字符串或者数值会自动建立一个包含值的对象及提供甚至能够在原始类型上运行的不一样方法。
另一个有趣的事实即诸如数组的复杂数据类型也是对象。当使用 typeof 来检查一个数组实例的时候会输出 object
。数组中每一个元素的索引值即对象的属性。因此经过数组索引来访问元素的时候,其实是在访问一个数组对象的属性而后得到属性值。当涉及到数据存储方式的时候,如下两种定义是相同的:
let names = [“SessionStack”]; let names = { “0”: “SessionStack”, “length”: 1 }
所以,访问数组元素和对象属性的速度是同样的。我走了不少弯路才发现该事实。之前有段时间,我得对项目中某段相当重要的代码进行大量的性能优化。当试验过其它简单的办法以后,我把全部的对象替换为数组。按理说,访问数组元素会比访问哈希图的键值更快。然而,我惊奇地发现没有半点性能的提高。在 JavaScript 中,全部的操做都是由访问哈希图中的键来实现的且耗时相同。
当谈到对象的时候,首先映上眼帘的即类。开发人员习惯于使用类和类之间的关联来组织程序。虽然 JavaScript 中一切皆对象,可是并无使用经典的基于类的继承。而是使用原型来实现继承。
在 JavaScript 中,每一个对象关联其原型对象。当访问对象的一个方法或属性的时候,首先在对象自身进行搜索。若是没有找到,则在对象原型上进行查找。
让咱们以定义基础类的构造函数为例:
function Component(content) { this.content = content; } Component.prototype.render = function() { console.log(this.content); }
在原型上添加 render 函数,这样 Component 的实例就可使用该方法。当调用该 Component 类实例的方法的时候,首先在实例上查询该方法。而后在原型上找到该渲染方法。
如今,尝试扩展 component 类,引入新的子类。
function InputField(value) { this.content = `<input type="text" value="${value}" />`; }
若是想要 InputField 扩展 component 类的方法且能够调用其 render 方法,就须要更改其原型。当调用子类的实例方法的时候,确定不但愿在一个空原型上进行查找(这里其实全部对象都一个共同的原型,这里原文不够严谨)。该查找会延续到 Component 类上。
InputField.prototype = Object.create(new Component());
这样,就能够在 Component 类的原型上找到 render 方法。为了实现继承,须要把 InputField 的原型设置为Component 类的实例。大多数库使用 Object.setPrototypeOf 来实现继承。
然而,还有其它事情须要作。每次扩展类,所须要作的事以下:
正如你所见,当想要实现全部基于类继承的功能的时候,每次都须要执行这么复杂的逻辑步骤。当须要建立这么多类的时候,即意味着须要把这些逻辑封装为可重用的函数。这就是开发者当初经过各类类库来模拟从而解决基于类的继承的问题。这些解决方案是如此流行,以致于迫切须要语言集成该功能。这就是为何 ECMAScript 2015 的第一个重要修订版中引入了支持基于类继承的建立类的语法。
当在 ES6 或者 ECMAScript 2015 中提议新功能时,JavaScript 开发者社区就火烧眉毛想要引擎和浏览器实现支持。一种好的实现方法即经过代码转换。它容许使用 ECMAScript 2015 来进行代码编写而后转换为任何浏览器都可以运行的 JavaScript 代码。这包括使用基于类的继承来编写类并转换为可执行代码。
Babel 是最为流行的转换器之一。让咱们经过 babel 转换 component 类来了解代码转换原理。
class Component { constructor(content) { this.content = content; } render() { console.log(this.content) } } const component = new Component('SessionStack'); component.render();
如下为 Babel 是如何转换类定义的:
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; }();
如你所见,代码被转换为可在任意环境中运行的 ECMAScript 5 代码。另外,引入了额外的函数。它们是 Babel 标准库的一部分。编译后的文件中引入了 _classCallCheck
和 _createClass
函数。第一个函数保证构造函数永远不会被当成普通函数调用。这是经过检查函数执行上下文是否为一个 Component 对象实例来实现的。代码检查 this 是否指向这样的实例。第二个函数 _createClass
经过传入包含键和值的对象数组来建立对象(类)的属性。
为了理解继承的工做原理,让咱们分析一下继承自 Component 类的 InputField 子类。
class InputField extends Component { constructor(value) { const content = `<input type="text" value="${value}" />`; super(content); } }
这里是使用 Babel 来处理以上示例的输出:
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);
本例中,在 _inherits 函数中封装了继承逻辑。它执行了前面所说的同样的操做即设置子类的原型为父类的实例。
为了转换代码,Babel 执行了几回转换。首先,解析 ES6 代码并转化成被称为语法抽象树的中间展现层,语法抽象树在以前的文章有讲过了。该树会被转换为一个不一样的语法抽象树,该树上每一个节点会转换为对应的 ECMAScript 5 节点。最后,把语法抽象树转换为 ES5 代码。
AST 由节点组成,每一个节点只有一个父节点。Babel 中有一种基础类型节点。该节点包含节点的内容及在代码中的位置的信息。有各类不一样类型的节点好比字面量表示字符串,数值,空值等等。也有控制流(if) 和 循环(for, while)的语句节点。另外,还有一种特殊类型的类节点。它是基础节点类的子类,经过添加字段变量来存储基础类的引用和把类的正文做为单独的节点来拓展自身。
转化如下代码片断为语法抽象树:
class Component { constructor(content) { this.content = content; } render() { console.log(this.content) } }
如下为该代码片断的语法抽象树的大概状况:
建立语法抽象树后,每一个节点转换为其对应的 ECMAScript 5 节点而后转化为遵循 ECMAScript 5 标准规范的代码。这是经过寻找离根节点最远的节点而后转换为代码。而后,他们的父节点经过使用每一个子节点生成的代码片断来转化为代码,依次类推。该过程被称为 depth-first traversal 即深度优先遍历。
以上示例,首先生成两个 MethodDefinition 节点,以后类正文节点的代码,最后是 ClassDeclaration 节点的代码。
TypeScript 是另外一个流行的框架。它引入了一种编写 JavaScript 程序的新语法,而后转换为任意浏览器或引擎能够运行的 EMCAScript 5 代码。如下为使用 Typescript 实现 component 类的代码:
class Component { content: string; constructor(content: string) { this.content = content; } render() { console.log(this.content) } }
如下为语法抽象树示意图:
一样支持继承。
class InputField extends Component { constructor(value: string) { const content = `<input type="text" value="${value}" />`; super(content); } }
代码转换结果以下:
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));
相似地,最后结果包含了一些来自 TypeScript 的类库代码。__extends
中封装了和以前第一部分讨论的同样的继承逻辑。
随着 Babel 和 TypeScript 的普遍使用,标准类和基于类的继承渐渐成为组织 JavaScript 程序的标准方式。这就推进了浏览器原生支持类。
2014 年,Chrome 原生支持类。这就能够不使用任意库或者转换器来实现声明类的语法。
类的原生实现的过程即被称为语法糖的过程。这只是一个优雅的语法能够被转换为语言早已支持的相同的原语。使用新的易用的类定义,归根结底也是要建立构造函数和修改原型。
让咱们了解下 V8 是如何原生支持 ES6 类的。如前面文章所讨论的那样,首先解析新语法为可运行的 JavaScript 代码并添加到 AST 树中。类定义的结果即在语法抽象树中添加一个 ClassLiteral 类型的新节点。
该节点包含了一些信息。首先,它把构造函数当成单独的函数且包含类属性集。这些属性能够是一个方法,一个 getter, 一个 setter, 一个公共变量或者私有变量。该节点还储存了指向父类的指针引用,该父类也并储存了构造函数,属性集和及父类引用,依次类推。
一旦把新的 ClassLiteral 转换为字节码,再将其转化为各类函数和原型。