JS的工作原理:类和继承,Babel和TypeScript中的转译

类是当今构建软件项目的最流行的方法之一。 这种编程方法也用在JavaScript中。 今天,我们将发布JS生态系统系列第15部分的翻译。 本文将讨论在JavaScript中实现类,继承机制和蒸腾的各种方法。 首先,我们将告诉您原型的工作原理,并分析各种方法来模拟流行的库中基于类的继承。 接下来,我们将讨论如何通过转译来编写JS程序,这些程序使用的功能不是该语言提供的,或者尽管它们以新标准的形式存在或处于不同批准阶段的提案,但尚未在JS-中实现引擎。 特别是,我们将讨论Babel和TypeScript以及ECMAScript 2015类,之后,我们将看一些示例,这些示例演示了V8 JS引擎中类的内部实现的功能。
图片


复习


在JavaScript中,即使看起来我们正在使用原始数据类型,我们也经常遇到对象。 例如,创建一个字符串文字:

const name = "SessionStack"; 

之后,我们可以立即使用name来调用String类型的对象的各种方法,我们创建的字符串文字将自动转换为该方法。

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

与其他语言不同,在JavaScript中,创建了一个包含例如字符串或数字的变量后,我们无需进行显式转换就可以使用此变量,就好像它最初是使用new关键字和相应的构造函数创建的一样。 结果,由于自动创建了封装原始值的对象,因此您可以像对待对象一样使用这些值,尤其是可以引用它们的方法和属性。

关于JavaScript类型系统的另一个值得注意的事实是,例如,数组也是对象。 如果查看为数组typeoftypeof命令的输出,则可以看到它报告所调查的实体具有object数据类型。 结果,事实证明数组元素的索引仅仅是特定对象的属性。 因此,当我们通过索引访问数组的元素时,它归结为使用Array类型的对象的属性并获取该属性的值。 如果我们讨论数据如何存储在普通对象和数组中,那么以下两种构造将导致创建几乎相同的数据结构:

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

结果,以相同的速度执行对数组元素和对象属性的访问。 本文的作者说,他是在解决一个复杂问题的过程中发现的。 即,一旦他需要对项目中非常重要的代码进行认真的优化。 在尝试了许多简单的方法之后,他决定用数组替换此代码中使用的所有对象。 从理论上讲,访问数组元素比使用哈希表键要快。 令他惊讶的是,这种替换丝毫不影响性能,因为使用数组和使用JavaScript中的对象归结为与哈希表的键进行交互,这两种情况都需要相同的时间。

使用原型模拟类


当我们考虑对象时,首先想到的是类。 也许今天从事编程的每个人都创建了应用程序,这些应用程序的结构基于类以及它们之间的关系。 尽管JavaScript的对象几乎可以在任何地方找到,但是该语言并未使用传统的基于类的继承系统。 JavaScript使用原型来解决类似的问题。


对象及其原型

在JavaScript中,每个对象都与另一个对象相关联-具有自己的原型。 当您尝试访问对象的属性或方法时,首先在对象本身中执行对所需内容的搜索。 如果搜索失败,则在对象的原型中继续搜索。

考虑一个简单的示例,该示例描述Component基类的构造函数:

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

在这里,我们将render()函数分配给原型方法,因为我们需要Component类的每个实例才能使用此方法。 在Component任何实例中,当调用render方法时,其搜索均始于为其调用对象。 然后在原型中继续搜索,系统在该原型中找到此方法。


原型和Component类的两个实例

现在,让我们尝试扩展Component类。 让我们为一个新类InputField创建一个构造函数:

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

如果我们需要InputFieldInputField扩展Component类的功能并能够调用其render方法,则需要更改其原型。 在子类的实例上调用方法时,在空的原型中查找它是没有意义的。 在搜索此方法时,我们需要在Component类中找到。 因此,我们需要执行以下操作:

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

现在,当使用InputField类的实例并调用Component类的方法时,可以在Component类的原型中找到此方法。 要实现继承系统,您需要将InputField原型连接到Component类的实例。 许多库使用Object.setPrototypeOf()解决此问题。


用InputField类扩展组件类

但是,上述动作不足以实现类似于传统继承的机制。 每次扩展类时,我们需要执行以下操作:

  • 使后代类的原型成为父类的实例。
  • 在后代类的构造函数中调用父类的构造函数,以确保正确初始化了父类。
  • 提供一种机制,用于在后代类重写父方法的情况下调用父类的方法,但是需要从父类调用此方法的原始实现。

如您所见,如果JS开发人员想要使用基于类的继承功能,则他将不得不不断执行上述步骤。 如果需要创建许多类,则可以采用适合重用的函数形式来完成所有这些工作。

实际上,以这种方式在JS开发的实践中最初解决了基于类组织继承的任务。 特别是使用各种库。 这样的解决方案变得非常流行,这清楚地表明JavaScript显然缺少某些东西。 因此,ECMAScript 2015引入了新的语法构造,旨在支持类的工作并实现相应的继承机制。

课堂翻译


在提出ECMAScript 2015(ES6)的新功能之后,JS社区希望尽快利用它们,而不必等待完成在JS引擎和浏览器中添加对这些功能的支持的漫长过程。 在解决这些问题中,移植是好的。 在这种情况下,编译过程简化为将根据ES6规则编写的JS代码转换为迄今为止不支持ES6功能的浏览器可以理解的视图。 结果,例如,可以声明类并根据ES6规则实现基于类的继承机制,并将这些构造转换为可在任何浏览器中使用的代码。 示意性地,该过程以编译器处理箭头功能为例(另一个需要时间支持的新语言功能)的示例可以表示为下图。


转译

Babel.js是最受欢迎的JavaScript编译器之一。 让我们通过执行上面讨论的Component类声明代码的编译来查看其工作原理。 这是ES6代码:

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

这是代码在编译后变成的内容:

 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代码是从transpiler的输出中获得的,可以在任何环境中运行。 此外,此处还添加了对Babel标准库一部分的某些函数的调用。

我们正在讨论转码中包含的_classCallCheck()_createClass()函数。 第一个函数_classCallCheck()旨在防止像常规函数一样调用构造函数。 为此,它检查在其中调用该函数的上下文是否是Component类的实例上下文。 代码检查以查看this关键字是否指向相似的实例。 第二个函数_createClass()创建对象属性,该属性作为包含键及其值的对象数组传递给它。

为了了解继承的工作原理,我们分析了InputField类,它是Component类的后代。 在ES6中,类关系如何结合在一起:

 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代码并将其转换为称为抽象语法树的中间表示形式。 然后将生成的抽象语法树转换为另一棵树,该树的每个节点都转换为其等效的ES5。 结果,该树被转换为JS代码。

Babel中的抽象语法树


抽象语法树包含节点,每个节点只有一个父节点。 Babel具有节点的基本类型。 它包含有关节点是什么以及在代码中可以找到它的位置的信息。 有多种类型的节点,例如,用于表示文字的节点,例如字符串,数字, null值等。 另外,还有用于表示用于控制程序执行流程的表达式的节点( if构造),以及用于循环的节点( forwhile )。 还有一种特殊的节点用于表示类。 它是Node基类的后代。 他通过添加用于存储对基类和作为单独节点的类主体的引用的字段来扩展此类。
将以下代码片段转换为抽象语法树:

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

这是他的示意图表示的外观。


抽象语法树

创建树后,将其每个节点转换为对应的ES5节点,然后将此新树转换为符合ECMAScript 5标准的代码,在转换过程中,首先找到离根节点最远的节点,然后将该节点转换为代码。使用为每个节点生成的摘要。 之后,重复该过程。 这种技术称为深度搜索

在上面的示例中,将首先生成两个MethodDefinition节点的代码, ClassBody将生成ClassBody节点的代码,最后生成ClassBody节点的代码。

TypeScript编译


另一种使用翻译的流行系统是TypeScript。 这是一种编程语言,其代码会转换为任何JS引擎都可以理解的ECMAScript 5代码。 它提供了用于编写JS应用程序的新语法。 这是在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 = `<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)); 

如您所见,这又是一个ES5代码,其中除了标准构造外,还调用了TypeScript库中的某些函数。 __extends()函数的功能类似于我们在本材料开头提到的功能。

由于Babel和TypeScript的广泛采用,用于声明类和组织基于类的继承的机制已成为构建JS应用程序的标准工具。 这有助于增加对浏览器中这些机制的支持。

浏览器类支持


类支持出现在2014年的Chrome浏览器中。 这使浏览器无需使用转译或任何辅助库即可使用类声明。


在Chrome JS控制台中使用类

实际上,浏览器对这些机制的支持不过是语法糖。 这些结构被转换为该语言已经支持的相同基本结构。 结果,即使您使用新的语法,在较低的层次上,一切看起来也像创建构造函数和操作对象的原型:


类支持是语法糖

V8中的类支持


让我们讨论一下ES6类支持如何在V8 JS引擎中工作。 在先前专门讨论抽象语法树的材料中 ,我们谈到了以下事实:在准备要执行的JS代码时,系统会对其进行解析并在其基础上形成一个抽象语法树。 解析类声明的构造时, ClassLiteral类型的节点将落入抽象语法树中。

这些节点存储了一些有趣的东西。 首先,它是作为单独函数的构造函数,其次,它是类属性的列表。 它可以是方法,获取器,设置器,公共或私有字段。 此外,此类节点还存储对父类的引用,该引用扩展了要为其形成节点的类,该类又存储了构造函数,属性列表以及指向其自身父类的链接。

将新的ClassLiteral节点转换为代码后 ,将其转换为由函数和原型组成的构造。

总结


该材料的作者说, SessionStack致力于尽可能全面地优化其库的代码,因为它必须解决收集有关网页上发生的所有事情的信息的艰巨任务。 在解决这些问题的过程中,库不应减慢所分析页面的工作。 此级别的优化需要考虑到影响性能的JavaScript生态系统的最小细节,尤其是要考虑到ES6中类和继承机制的排列方式。

亲爱的读者们! 您是否使用ES6语法构造来处理JavaScript中的类?

Source: https://habr.com/ru/post/zh-CN415377/


All Articles