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

HowJavascriptworks(Javascript工作原理)(十五)类和继承及Babel和TypeScript代码转换探秘

个人总结:读完这篇文章需要15分钟,文章主要讲解了Babel和TypeScript的工作原理,(例如对es6类的转换,是将原始es6代码转换为es5代码,这些代码中包含着类似于_c
个人总结:读完这篇文章需要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= ``;
}

如果想要 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= ``;
        super(content);
    }
}

这里是使用 Babel 来处理以上示例的输出:


var InputField = function (_Component) {
  _inherits(InputField, _Component);

  function InputField(value) {
    _classCallCheck(this, InputField);

    var cOntent= ‘‘;
    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= ``;
        super(content);
    }
}

代码转换结果如下:

var InputField = /** @class */ (function (_super) {
    __extends(InputField, _super);
    function InputField(value) {
        var _this = this;
        var cOntent= "";
        _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 代码转换探秘


推荐阅读
  • 本文深入解析了Java面向对象编程的核心概念及其应用,重点探讨了面向对象的三大特性:封装、继承和多态。封装确保了数据的安全性和代码的可维护性;继承支持代码的重用和扩展;多态则增强了程序的灵活性和可扩展性。通过具体示例,文章详细阐述了这些特性在实际开发中的应用和优势。 ... [详细]
  • 在探讨Hibernate框架的高级特性时,缓存机制和懒加载策略是提升数据操作效率的关键要素。缓存策略能够显著减少数据库访问次数,从而提高应用性能,特别是在处理频繁访问的数据时。Hibernate提供了多层次的缓存支持,包括一级缓存和二级缓存,以满足不同场景下的需求。懒加载策略则通过按需加载关联对象,进一步优化了资源利用和响应时间。本文将深入分析这些机制的实现原理及其最佳实践。 ... [详细]
  • PHP自学必备:从零开始的准备工作与工具选择 ... [详细]
  • 初探性能优化:入门指南与实践技巧
    在编程领域,常有“尚未精通编码便急于优化”的声音。为了从性能优化的角度提升代码质量,本文将带领读者初步探索性能优化的基本概念与实践技巧。即使程序看似运行良好,数据处理效率仍有待提高,通过系统学习性能优化,能够帮助开发者编写更加高效、稳定的代码。文章不仅介绍了性能优化的基础知识,还提供了实用的调优方法和工具,帮助读者在实际项目中应用这些技术。 ... [详细]
  • 作为软件工程专业的学生,我深知课堂上教师讲解速度之快,很多时候需要课后自行消化和巩固。因此,撰写这篇Java Web开发入门教程,旨在帮助初学者更好地理解和掌握基础知识。通过详细记录学习过程,希望能为更多像我一样在基础方面还有待提升的学员提供有益的参考。 ... [详细]
  • ButterKnife 是一款用于 Android 开发的注解库,主要用于简化视图和事件绑定。本文详细介绍了 ButterKnife 的基础用法,包括如何通过注解实现字段和方法的绑定,以及在实际项目中的应用示例。此外,文章还提到了截至 2016 年 4 月 29 日,ButterKnife 的最新版本为 8.0.1,为开发者提供了最新的功能和性能优化。 ... [详细]
  • 本文详细介绍了使用 Python 进行 MySQL 和 Redis 数据库操作的实战技巧。首先,针对 MySQL 数据库,通过 `pymysql` 模块展示了如何连接和操作数据库,包括建立连接、执行查询和更新等常见操作。接着,文章深入探讨了 Redis 的基本命令和高级功能,如键值存储、列表操作和事务处理。此外,还提供了多个实际案例,帮助读者更好地理解和应用这些技术。 ... [详细]
  • NOIP2000的单词接龙问题与常见的成语接龙游戏有异曲同工之妙。题目要求在给定的一组单词中,从指定的起始字母开始,构建最长的“单词链”。每个单词在链中最多可出现两次。本文将详细解析该题目的解法,并分享学习过程中的心得体会。 ... [详细]
  • ### 摘要`mkdir` 命令用于在指定位置创建新的目录。其基本格式为 `mkdir [选项] 目录名称`。通过该命令,用户可以在文件系统中创建一个或多个以指定名称命名的文件夹。执行此操作的用户需要具备相应的权限。此外,`mkdir` 还支持多种选项,如 `-p` 用于递归创建多级目录,确保路径中的所有层级都存在。掌握这些基本用法和选项,有助于提高在 Linux 系统中的文件管理效率。 ... [详细]
  • 在Android平台上,视频监控系统的优化与应用具有重要意义。尽管已有相关示例(如http:www.open-open.comlibviewopen1346400423609.html)展示了基本的监控功能实现,但若要提升系统的稳定性和性能,仍需进行深入研究和优化。本文探讨了如何通过改进算法、优化网络传输和增强用户界面来提高Android视频监控系统的整体效能,以满足更复杂的应用需求。 ... [详细]
  • 将解压缩版Tomcat集成至系统服务
    将解压缩版Tomcat集成至系统服务的方法如下:首先,在命令行中导航至Tomcat的`bin`目录,运行`service.bat install`命令以安装服务。需要注意的是,服务名称和显示名称已在`service.bat`脚本中预设,默认情况下会随不同版本有所变化。此外,建议检查并配置相关参数,确保服务能够稳定运行。 ... [详细]
  • 题目探讨了在无向图中求解点连通数的问题,具体涉及UVA1660和POJ1966两个经典问题。通过最小割算法的应用,分析了如何高效地确定网络中的关键节点和路径,为电缆电视网络的优化设计提供了理论支持。该研究不仅验证了最小割算法的有效性,还为进一步探索复杂网络的连通性和鲁棒性奠定了基础。 ... [详细]
  • 在 POJ1651 的乘法谜题挑战中,如果选手按相反顺序选择卡片,即先选 50,再选 20,最后选 1,则最终得分会有所不同。题目要求输入的第一行包含... 改写后的摘要:在 POJ1651 的乘法谜题挑战中,如果选手按照逆序选取卡片,例如依次选择 50、20 和 1,最终的得分将发生变化。题目首先要求输入的第一行包括... ... [详细]
  • 资源管理器的基础架构包括三个核心组件:1)资源池,用于将CPU和内存等资源分配给不同的容器;2)负载组,负责承载任务并将其分配到相应的资源池;3)分类函数,用于将不同的会话映射到合适的负载组。该系统提供了两种主要的资源管理策略。 ... [详细]
  • AngularJS 进阶指南:第三部分深入解析
    在本文中,我们将深入探讨 AngularJS 的指令模型,特别是 `ng-model` 指令。`ng-model` 指令用于将 HTML 元素与应用程序数据进行双向绑定,支持多种数据类型验证,如数字、电子邮件地址和必填项检查。此外,我们还将介绍如何利用该指令优化表单验证和数据处理流程,提升开发效率和用户体验。 ... [详细]
author-avatar
dreamingsue
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有