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

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

想阅读更多优质文章请猛戳GitHub博客,一年百来篇优质文章等着你!前端

若是你错过了前面的章节,能够在这里找到它们:java

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

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

概述

在 JavaScript 中,没有基本类型,建立的全部东西都是对象。例如,建立一个新字符串:github

const name = "SessionStack";

接着在新建立的对象上调用不一样的方法:web

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

与其余语言不一样,在 JavaScript 中,字符串或数字的声明会自动建立一个封装值的对象,并提供不一样的方法,甚至能够在基本类型上执行这些方法。chrome

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

let names = [“SessionStack”];

let names = {
  “0”: “SessionStack”,
  “length”: 1
}

所以,访问数组中的元素和对象的属性耗时是相同的。我(本文做者)经过屡次的努力才发现这一点的。就是不久,我(本文做者)不得不对项目中的一段关键代码进行大规模优化。在尝试了全部简单的可选项以后,最后用数组替换了项目中使用的全部对象。理论上,访问数组中的元素比访问哈希映射中的键要快且对性能没有任何影响。在 JavaScript中,这两种操做都是做为访问哈希映射中的键来实现的,而且花费相同的时间。c#

使用原型模拟类

通常的想到对象时,首先想到的是类。咱们大都习惯于根据类及其之间的关系来构建应用程序。尽管 JavaScript 中的对象无处不在,但该语言并不使用传统的基于类的继承,相反,它依赖于原型来实现。segmentfault

图片描述

在 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 = `<input type="text" value="${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 = `<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);

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

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

  1. 编写ES6代码
  2. babylon 进行解析
  3. 解析获得 AST
  4. plugin 用 babel-traverse 对 AST 树进行遍历转译
  5. 获得新的 AST树
  6. 用 babel-generator 经过 AST 树生成 ES5 代码

Babel 中的抽象语法树

AST 包含多个节点,且每一个节点只有一个父节点。 在 Babel 中,每一个形状树的节点包含可视化类型、位置、在树中的链接等信息。 有不一样类型的节点,如 stringnumbersnull等,还有用于流控制(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 = `<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));

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

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

类的原生支持

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

图片描述

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

图片描述

V8的支持

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

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

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


原文:

https://blog.sessionstack.com...

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

你的点赞是我持续分享好东西的动力,欢迎点赞!

交流

干货系列文章汇总以下,以为不错点个Star,欢迎 加群 互相学习。

https://github.com/qq44924588...

我是小智,公众号「大迁世界」做者,对前端技术保持学习爱好者。我会常常分享本身所学所看的干货,在进阶的路上,共勉!

关注公众号,后台回复福利,便可看到福利,你懂的。

clipboard.png