How Javascript works (Javascript工作原理) (十五) 類和繼承及 Babel 和 TypeScript 代碼轉換探秘

來源:互聯網
上載者:User

標籤:ssl   inpu   UNC   同方   開發人員   理解   reference   ack   inherits   

個人總結:讀完這篇文章需要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)); // 輸出 SessionStackSessionStackconsole.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 = `<input type="text" value="${value}" />`;}

如果想要 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 = `<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 代碼並轉化成被稱為文法抽象樹的中間展示層,文法抽象樹在之前的文章有講過了。該樹會被轉換為一個不同的文法抽象樹,該樹上每個節點會轉換為對應的 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 = `<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));

類似地,最後結果包含了一些來自 TypeScript 的類庫代碼。__extends 中封裝了和之前第一部分討論的一樣的繼承邏輯。

隨著 Babel 和 TypeScript 的廣泛使用,標準類和基於類的繼承漸漸成為組織 JavaScript 程式的標準方式。這就推動了瀏覽器原生支援類。

 

類的原生支援

2014 年,Chrome 原生支援類。這就可以不使用任意庫或者轉換器來實現聲明類的文法。

類的原生實現的過程即被稱為文法糖的過程。這隻是一個優雅的文法可以被轉換為語言早已支援的相同的原語。使用新的易用的類定義,歸根結底也是要建立建構函式和修改原型。

V8 引擎支援情況

讓我們瞭解下 V8 是如何原生支援 ES6 類的。如前面文章所討論的那樣,首先解析新文法為可啟動並執行 JavaScript 代碼並添加到 AST 樹中。類定義的結果即在文法抽象樹中添加一個 ClassLiteral 類型的新節點。

該節點包含了一些資訊。首先,它把建構函式當成單獨的函數且包含類屬性集。這些屬性可以是一個方法,一個 getter, 一個 setter, 一個公開變數或者私人變數。該節點還儲存了指向父類的指標引用,該父類也並儲存了建構函式,屬性集和及父類引用,依次類推。

一旦把新的 ClassLiteral 轉換為位元組碼,再將其轉化為各種函數和原型。

 

How Javascript works (Javascript工作原理) (十五) 類和繼承及 Babel 和 TypeScript 代碼轉換探秘

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.