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

相關文章
相關標籤/搜索