当前位置: 首页 > Web前端 > CSS

JavaScript的工作原理十五——类与继承与Babel与TypeScript代码转换探索

时间:2023-03-30 22:52:51 CSS

此处请查看原文,略有删节,本文采用知识共享署名4.0国际许可协议共享,BYTroland。本系列持续更新中,Github地址请查看这里。这是JavaScript工作原理的第15章。用类来组织各种软件工程代码是现在最普遍的方式。本章探讨了实现JavaScript类的不同方法以及如何构建类继承。我们将深入了解原型继承,并使用流行的类库分析基于类的继承的实现。接下来,我将向您展示如何使用转译器向语言添加非本机支持的语法功能,以及如何在Babel和TypeScript中使用它们来支持ECMAScript2015类。最后介绍几个V8原生支持实现类的例子。概述JavaScript没有原始类型,一切都是对象。例如,以下字符串:constname="SessionStack";可以立即在新创建的对象上调用不同的方法:console.log(a.repeat(2));//输出SessionStackSessionStackconsole.log(a.toLowerCase());//OutputsessionstackJavaScript与其他语言不同,声明一个字符串或值会自动创建一个包含该值的对象,并提供甚至可以对基本类型进行操作的不同方法。另一个有趣的事实是,数组等复杂数据类型也是对象。当使用typeof检查数组实例时,将输出对象。数组中每个元素的索引值是对象的属性。所以通过数组索引访问元素时,实际上是在访问数组对象的一个??属性,然后获取该属性值。说到数据存储,下面两个定义是一样的:letnames=["SessionStack"];letnames={"0":"SessionStack","length":1}因此,访问数组元素和对象属性的速度是一样的。我走了很多弯路才发现这个事实。曾经有一段时间,我不得不对项目中的关键代码段进行大量性能优化。在尝试了其他简单的解决方案后,我将所有对象替换为数组。按理说,访问数组的元素会比访问hashmap的键更快。但是,令我惊讶的是没有性能提升。在JavaScript中,所有操作都是通过访问hashmap中的键来实现的,并且花费相同的时间。用原型对类建模说到对象,首先映入眼帘的是类。开发人员习惯于使用类和类之间的关联来组织程序。尽管JavaScript中的一切都是对象,但它并没有使用经典的基于类的继承。相反,使用原型来实现继承。在JavaScript中,每个对象都与其原型对象相关联。当访问对象的方法或属性时,首先搜索对象本身。如果未找到,则对对象原型执行查找。下面以定义基类的构造函数为例:原型函数,以便Component的实例可以使用该方法。当调用Component类实例的方法时,首先在实例上查询该方法。然后找到原型上的渲染方法。现在,尝试扩展组件类,引入一个新的子类。functionInputField(value){this.content=``;}如果想让InputField扩展组件类的方法并调用它的render方法,你需要改变它的原型。在调用子类的实例方法时,肯定不想在空原型上搜索(这里,其实所有的对象都有一个共同的原型,这里的原文不够严谨)。此查找会延续到Component类。InputField.prototype=Object.create(newComponent());这样就可以在Component类的原型上找到render方法了。为了实现继承,需要将InputField的原型设置为Component类的一个实例。大多数库使用Object.setPrototypeOf来实现继承。但是,还有其他事情需要做。每次扩展一个类,需要做的事情如下:将子类的原型设置为父类的一个实例,并在子类构造函数中调用父类构造函数,这样父类构造函数的初始化逻辑可以执行。介绍访问父类的方法。覆盖超类方法时,您想调用超类方法的原始实现。正如你所看到的,当你想要实现基于类继承的所有功能时,你每次都需要执行如此复杂的逻辑步骤。当需要创建如此多的类时,意味着需要将这些逻辑封装为可重用的函数。这就是过去开发者通过各种类库来模拟解决类继承问题的问题。这些解决方案非常受欢迎,因此迫切需要语言集成此功能。这就是为什么ECMAScript2015的第一次重大修订引入了支持基于类继承创建类的语法。类转换当ES6或ECMAScript2015中提出新功能时,JavaScript开发者社区迫不及待地等待引擎和浏览器实现支持。执行此操作的一个好方法是通过转码。它允许将使用ECMAScript2015编写的代码转换为任何浏览器都可以运行的JavaScript代码。这包括使用基于类的继承编写类并将它们转换为可执行代码。Babel是最流行的转译器之一。下面我们通过babel转换组件类,看看转码是如何工作的。classComponent{constructor(content){this.content=content;}render(){console.log(this.content)}}constcomponent=newComponent('SessionStack');component.render();以下是Babel如何转换类定义:varComponent=function(){functionComponent(content){_classCallCheck(this,Component);this.content=内容;}_createClass(Component,[{key:'render',value:functionrender(){console.log(this.content);}}]);返回组件;}();如您所见,代码已转换为可在任何环境中运行的ECMAScript5代码。此外,还引入了附加功能。它们是Babel标准库的一部分。编译后的文件引入了_classCallCheck和_createClass函数。第一个函数保证构造函数永远不会作为普通函数被调用。这是通过检查函数执行上下文是否是组件对象实例来完成的。代码检查this是否指向这样的实例。第二个函数_createClass通过传入包含键和值的对象数组来创建对象(类)的属性。为了理解继承是如何工作的,让我们分析一下从Component类继承的InputField子类。classInputFieldextendsComponent{constructor(value){constcontent=``;超级(内容);}}下面是上述示例使用Babel的输出:varInputField=function(_Component){_inherits(InputField,_Component);函数InputField(value){_classCallCheck(this,InputField);varcontent='';return_possibleConstructorReturn(this,(InputField.__proto__||Object.getPrototypeOf(InputField)).call(this,content));}返回输入字段;}(组件);在这个例子中,_inherits函数封装了继承逻辑。它执行与上述相同的操作,即设置子类的原型为父类的实例。为了转换代码,Babel执行了几个转换。首先,ES6代码被解析并转换成一个中间表示层,称为语法抽象树,这在上一篇文章中有提到。该树被转换为不同的句法抽象树,其中每个节点都被转换为相应的ECMAScript5节点。最后将句法抽象树转成ES5代码。Babel中的句法抽象树AST由节点组成,每个节点只有一个父节点。Babel中有一个基本类型节点。此节点包含有关节点内容及其在代码中的位置的信息。有各种类型的节点,例如表示字符串、数字、空值等的文字。还有用于控制流(if)和循环(for、while)的语句节点。此外,还有一种特殊类型的类节点。它是基节点类的子类,通过添加字段变量来存储对基类的引用并将类的文本视为单个节点来扩展自身。将以下代码片段转换为语法抽象树:classComponent{constructor(content){this.content=content;}render(){console.log(this.content)}}下面是这段代码片段的语法抽象树的大概情况:语法抽象树创建完成后,每个节点都转换为对应的ECMAScript5节点,然后转换为遵循ECMAScript5标准规范的代码。这是通过找到离根节点最远的节点然后转换为代码来完成的。然后,使用每个子节点生成的片段将它们的父节点转换为代码,依此类推。这个过程称为深度优先遍历或深度优先遍历。上面的示例首先生成两个MethodDefinition节点,然后是ClassLiteral节点的代码,最后是ClassDeclaration节点的代码。使用TypeScript进行转换TypeScript是另一个流行的框架。它引入了一种用于编写JavaScript程序的新语法,然后将其转换为任何浏览器或引擎都可以运行的EMCAScript5代码。下面是使用Typescript实现组件类的代码:classComponent{content:string;构造函数(内容:字符串){this.content=content;}render(){console.log(this.content)}}下面是语法抽象树示意图:也支持继承。classInputFieldextendsComponent{constructor(value:string){constcontent=``;超级(内容);}}代码转换结果如下:varInputField=/**@class*/(function(_super){__extends(InputField,_super);functionInputField(value){var_this=this;varcontent="";_this=_super.call(this,content)||this;return_this;}returnInputField;}(Component));同样,最终结果包含一些来自TypeScript的库代码。__extends封装了与上面第1部分中讨论的相同的继承逻辑。随着Babel和TypeScript的广泛使用,标准类和基于类的继承正在成为组织JavaScript程序的标准方式。这驱动了浏览器本机支持类。类的原生支持2014年,Chrome原生支持类。这启用了无需使用任何库或转换器即可声明类的语法。类本机实现的过程称为语法糖的过程。它只是一种优雅的语法,可以转换为该语言已经支持的相同原语。使用新的易于使用的类定义归结为创建构造函数和修改原型。V8引擎支持让我们看看V8如何原生支持ES6类。如上一篇文章所述,新语法首先被解析为可运行的JavaScript代码并添加到AST树中。类定义的结果是向句法抽象树添加了一个类型为ClassLiteral的新节点。该节点包含一些信息。首先,它将构造函数视为具有一组类属性的单个函数。这些属性可以是方法、getter、setter、公共变量或私有变量。该节点还存储了指向父类的指针引用,其中还存储了构造函数、属性集和父类引用等。一旦新的ClassLiteral被转换成字节码,它就会被转换成各种函数和原型。本系列持续更新中,Github地址请查看这里。