How Javascript works (Javascript工作原理) (十五) 类和继承及 Babel 和 TypeScript 代码转换探秘
个人总结:读完这篇文章需要15分钟,文章主要讲解了Babel和TypeScript的工作原理,(例如对es6 类的转换,是将原始es6代码转换为es5代码,这些代码中包含着类似于 _classCallCheck 和 _createClass这样的函数,而这些函数已经
在Babel和TypeScript的标准库中预先定义好了,然后进行处理)。
顺便温习了Object.create这个方法, 比如有一个obj:{name:'是ho',f:function(){alert(1)}}
var a = Object.create(obj)
这时,a对象的原型就是这个obj
以上等同于
var a
a = {} //q是一个对象
a.__proto__= obj
类和继承及 Babel 和 TypeScript 代码转换探秘
这是 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 代码。
Babel 中的语法抽象树
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 进行转换
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 引擎支持情况
让我们了解下 V8 是如何原生支持 ES6 类的。如前面文章所讨论的那样,首先解析新语法为可运行的 JavaScript 代码并添加到 AST 树中。类定义的结果即在语法抽象树中添加一个 ClassLiteral 类型的新节点。
该节点包含了一些信息。首先,它把构造函数当成单独的函数且包含类属性集。这些属性可以是一个方法,一个 getter, 一个 setter, 一个公共变量或者私有变量。该节点还储存了指向父类的指针引用,该父类也并储存了构造函数,属性集和及父类引用,依次类推。
一旦把新的 ClassLiteral 转换为字节码,再将其转化为各种函数和原型。
How Javascript works (Javascript工作原理) (十五) 类和继承及 Babel 和 TypeScript 代码转换探秘的更多相关文章
- JavaScript是如何工作的:深入类和继承内部原理 + Babel和TypeScript 之间转换
这是专门探索 JavaScript 及其所构建的组件的系列文章的第 15 篇. 如果你错过了前面的章节,可以在这里找到它们: JavaScript 是如何工作的:引擎,运行时和调用堆栈的概述! Jav ...
- JavaScript是如何工作的:深入类和继承内部原理 + Babel和TypeScript之间转换
现在构建任何类型的软件项目最流行的方法这是使用类.在这篇文章中,探讨用 JavaScript 实现类的不同方法,以及如何构建类的结构.首先从深入研究原型工作原理,并分析在流行库中模拟基于类的继承的方法 ...
- How Javascript works (Javascript工作原理) (一) 引擎,运行时,函数调用栈
个人总结:该系列文章对JS底层的工作原理进行了介绍. 这篇文章讲了 运行时:js其实是和AJAX.DOM.Settimeout等WebAPI独立分离开的 调用栈:JavaScript的堆内存管理 和 ...
- How Javascript works (Javascript工作原理) (四) 事件循环及异步编程的出现和 5 种更好的 async/await 编程方式
个人总结: 1.讲解了JS引擎,webAPI与event loop合作的机制. 2.setTimeout是把事件推送给Web API去处理,当时间到了之后才把setTimeout中的事件推入调用栈. ...
- JavaScript定时器的工作原理(翻译)
JavaScript定时器的工作原理(翻译) 标签(空格分隔): JavaScript定时器 最近在看ajax原理的时候,看到了一篇国外的文章,讲解了JavaScript定时器的工作原理,帮助我很好的 ...
- [中英对照]How PCI Works | PCI工作原理
How PCI Works | PCI工作原理 Your computer's components work together through a bus. Learn about the PCI ...
- [中英对照]How PCI Express Works | PCIe工作原理
How PCI Express Works | PCIe工作原理 PCI Express is a high-speed serial connection that operates more li ...
- Python进阶(十五)----面向对象之~继承(单继承,多继承MRO算法)
Python进阶(十五)----面向对象之~继承 一丶面向对象的三大特性:封装,继承,多态 二丶什么是继承 # 什么是继承 # b 继承 a ,b是a的子类 派生类 , a是b的超类 基类 父类 # ...
- How Javascript works (Javascript工作原理) (五) 深入理解 WebSockets 和带有 SSE 机制的HTTP/2 以及正确的使用姿势
个人总结: 1.长连接机制——分清Websocket,http2,SSE: 1)HTTP/2 引进了 Server Push 技术用来让服务器主动向客户端缓存发送数据.然而,它并不允许直接向客户端程序 ...
随机推荐
- git相关整理
title: git相关整理 toc: false date: 2018-09-24 20:42:55 git merge 和 git merge --no--ff有什么区别? git merge命令 ...
- BZOJ 2190 欧拉函数
思路: 递推出来欧拉函数 搞个前缀和 sum[n-1]*2+3就是答案 假设仪仗队是从零开始的 视线能看见的地方就是gcd(x,y)=1的地方 倒过来一样 刨掉(1,1) 就是ans*2+1 再加一下 ...
- 前端模块化 | 解读JS模块化开发中的 require、import 和 export
本篇分为两个部分 第一部分:总结了ES6出现之前,在当时现有的运行环境中,实现"模块"的方式: 第二部分:总结了ES6出现后,module成为ES6标准,客户端实现模块化的解决方案 ...
- 《Unix环境高级编程》读书笔记 第11章-线程
1. 引言 了解如何使用多个控制线程在单进程环境中执行多个任务. 不管在什么情况下,只要单个资源需要在多个用户键共享,就必须处理一致性问题. 2. 线程概念 典型的Unix进程可以看成只有一个控制线程 ...
- CF474F Ant colony
#include<iostream> #include<cstring> #include<cstdio> #include<algorithm> #i ...
- [SHOI2009]Booking 会场预约
题目:洛谷P2161. 题目大意:有一些操作,分为两种: A.增加一个从第l天到第r天的预约,并删除与这个预约冲突的其他预约,输出删除了多少个预约. B.输出当前有效预约个数. 两个预约冲突定义为两个 ...
- windows下命令行复制
在CMD命令提示符窗口中点击鼠标右键,选择“标记”选项,然后按住鼠标左键不动,拖动鼠标标记想要复制的内容.标记完成以后请按键盘上的“回车”键
- mariadb数据库基础知识及备份
数据库介绍 1.什么是数据库? 简单的说,数据库就是一个存放数据的仓库,这个仓库是按照一定的数据结构(数据结构是指数据的组织形式或数据之间的联系)来组织,存储的,我们可以通过数据库提供的多种方法来管理 ...
- 2015 Multi-University Training Contest 2 hdu 5303 Delicious Apples
Delicious Apples Time Limit: 5000/3000 MS (Java/Others) Memory Limit: 524288/524288 K (Java/Other ...
- cogs 181. [USACO Jan07] 最高的牛
181. [USACO Jan07] 最高的牛 ★★ 输入文件:tallest.in 输出文件:tallest.out 简单对比时间限制:1 s 内存限制:32 MB FJ's N ( ...