热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

JavaScript是如何工作的:深入类和继承内部原理+Babel和TypeScript之间转换

摘要:深入JS系列15。原文:深入类和继承内部原理Babel和TypeScript之间转换作者:前端小智Fundebug经授权转载&#

摘要: 深入JS系列15。

  • 原文:深入类和继承内部原理 + Babel和TypeScript 之间转换
  • 作者:前端小智

Fundebug经授权转载,版权归原作者所有。

这是专门探索 Javascript 及其所构建的组件的系列文章的第 15 篇。

如果你错过了前面的章节,可以在这里找到它们:

  • Javascript 是如何工作的:引擎,运行时和调用堆栈的概述!
  • Javascript 是如何工作的:深入V8引擎&编写优化代码的5个技巧!
  • Javascript 是如何工作的:内存管理+如何处理4个常见的内存泄漏 !
  • Javascript 是如何工作的:事件循环和异步编程的崛起+ 5种使用 async/await 更好地编码方式!
  • Javascript 是如何工作的:深入探索 websocket 和HTTP/2与SSE +如何选择正确的路径!
  • Javascript 是如何工作的:与 WebAssembly比较 及其使用场景 !
  • Javascript 是如何工作的:Web Workers的构建块+ 5个使用他们的场景!
  • Javascript 是如何工作的:Service Worker 的生命周期及使用场景!
  • Javascript 是如何工作的:Web 推送通知的机制!
  • Javascript是如何工作的:使用 MutationObserver 跟踪 DOM 的变化!
  • Javascript是如何工作的:渲染引擎和优化其性能的技巧!
  • Javascript是如何工作的:深入网络层 + 如何优化性能和安全!
  • Javascript是如何工作的:CSS 和 JS 动画底层原理及如何优化它们的性能!
  • Javascript是如何工作的:解析、抽象语法树(AST)+ 提升编译速度5个技巧!

现在构建任何类型的软件项目最流行的方法这是使用类。在这篇文章中,探讨用 Javascript 实现类的不同方法,以及如何构建类的结构。首先从深入研究原型工作原理,并分析在流行库中模拟基于类的继承的方法。 接下来是讲如何将新的语法转制为浏览器识别的语法,以及在 Babel 和 TypeScript 中使用它来引入ECMAScript 2015类的支持。最后,将以一些在 V8 中如何本机实现类的示例来结束本文。

概述

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

const name = "SessionStack";

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

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

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

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

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);
}

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

接着引入一个新的子类:

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

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

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

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

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

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

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

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

类的转换

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

Babel

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

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;
}();

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

_classCallCheck_createClass 作为函数包含在编译文件中。

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

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

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

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

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

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

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

  • 编写ES6代码
  • babylon 进行解析
  • 解析得到 AST
  • plugin 用 babel-traverse 对 AST 树进行遍历转译
  • 得到新的 AST树
  • 用 babel-generator 通过 AST 树生成 ES5 代码

代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug。

Babel 中的抽象语法树

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

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

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

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

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

解析

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

转换

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

生成

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

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

使用 TypeScript 进行转换

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

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

转成抽象语法树如下:

Typescript 还支持继承:

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

以下是转换结果:

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

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

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

类的原生支持

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

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

V8的支持

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

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

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

原文:How Javascript works: The internals of classes and inheritance + transpiling in Babel and TypeScript

关于Fundebug

Fundebug专注于Javascript、微信小程序、微信小游戏、支付宝小程序、React Native、Node.js和Java线上应用实时BUG监控。 自从2016年双十一正式上线,Fundebug累计处理了9亿+错误事件,付费客户有Google、360、金山软件、百姓网等众多品牌企业。欢迎大家免费试用!


推荐阅读
  • Java序列化对象传给PHP的方法及原理解析
    本文介绍了Java序列化对象传给PHP的方法及原理,包括Java对象传递的方式、序列化的方式、PHP中的序列化用法介绍、Java是否能反序列化PHP的数据、Java序列化的原理以及解决Java序列化中的问题。同时还解释了序列化的概念和作用,以及代码执行序列化所需要的权限。最后指出,序列化会将对象实例的所有字段都进行序列化,使得数据能够被表示为实例的序列化数据,但只有能够解释该格式的代码才能够确定数据的内容。 ... [详细]
  • 如何使用Java获取服务器硬件信息和磁盘负载率
    本文介绍了使用Java编程语言获取服务器硬件信息和磁盘负载率的方法。首先在远程服务器上搭建一个支持服务端语言的HTTP服务,并获取服务器的磁盘信息,并将结果输出。然后在本地使用JS编写一个AJAX脚本,远程请求服务端的程序,得到结果并展示给用户。其中还介绍了如何提取硬盘序列号的方法。 ... [详细]
  • flowable工作流 流程变量_信也科技工作流平台的技术实践
    1背景随着公司业务发展及内部业务流程诉求的增长,目前信息化系统不能够很好满足期望,主要体现如下:目前OA流程引擎无法满足企业特定业务流程需求,且移动端体 ... [详细]
  • Imtryingtofigureoutawaytogeneratetorrentfilesfromabucket,usingtheAWSSDKforGo.我正 ... [详细]
  • 基于Axis、XFire、CXF的webservice客户端调用示例
    本文介绍了如何使用Axis、XFire、CXF等工具来实现webservice客户端的调用,以及提供了使用Java代码进行调用的示例。示例代码中设置了服务接口类、地址,并调用了sayHello方法。 ... [详细]
  • 本文记录了在vue cli 3.x中移除console的一些采坑经验,通过使用uglifyjs-webpack-plugin插件,在vue.config.js中进行相关配置,包括设置minimizer、UglifyJsPlugin和compress等参数,最终成功移除了console。同时,还包括了一些可能出现的报错情况和解决方法。 ... [详细]
  • Servlet多用户登录时HttpSession会话信息覆盖问题的解决方案
    本文讨论了在Servlet多用户登录时可能出现的HttpSession会话信息覆盖问题,并提供了解决方案。通过分析JSESSIONID的作用机制和编码方式,我们可以得出每个HttpSession对象都是通过客户端发送的唯一JSESSIONID来识别的,因此无需担心会话信息被覆盖的问题。需要注意的是,本文讨论的是多个客户端级别上的多用户登录,而非同一个浏览器级别上的多用户登录。 ... [详细]
  • 前端简史之纵横:Node东出
    引💡Ajax的出现,带来了jQuery时代,而jQuery时代也伴随着Node风暴淡淡退出了历史舞台。如果说Ajax给前端带来了从网页静 ... [详细]
  • 小编给大家分享一下TypeScript2.7有什么改进,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收 ... [详细]
  • 枚举使用枚举我们可以定义一些带名字的常量。使用枚举可以清晰地表达意图或创建一组有区别的用例。TypeScript支持数字的和基于字符串的枚举。数字枚举首先我们看看数字枚举,如果你使 ... [详细]
  • Java手机看书软件(一)魔屏3.0魔屏(MoScreen)是一款手机漫画阅读器,可以查找、下载阅读多种格式的富媒体内容.魔屏3.0主要功能:漫画分类:支持分类浏览漫画名称、简介、 ... [详细]
  • php中session_id()函数原型及说明session_id()函数说明:stringsession_id([string$id])session_id()可以用来获取设置当 ... [详细]
  • 最简便的 JavaScript 代码检查工具安装方式
    前两天发了一篇用mingw编译javascriptv8,讲述我为了在Windows下给javascript做lint检查,费劲去编译google的jav ... [详细]
  • 浏览器如何工作(How browsers work)的阅读笔记
    浏览器如何工作(Howbrowserswork)的阅读笔记1.整体结构完整的浏览器整体框架的发改如下:UI:就是那些我们常常 ... [详细]
  • 前言小伙伴们大家好。从今天开始我们将从 ... [详细]
author-avatar
11
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有