原文請查閱這裏,略有刪減,本文采用知識共享署名 4.0 國際許可協議共享,BY Troland。javascript
本系列持續更新中,Github 地址請查閱這裏。java
這是 JavaScript 工做原理的第十五章。git
現在使用類來組織各類軟件工程代碼是最經常使用的方法。本章將會探索實現 JavaScript 類的不一樣方法及如何構建類繼承。咱們將深刻理解原型繼承及分析使用流行的類庫模擬實現基於類繼承的方法。接下來,將會介紹如何使用轉換器爲語言添加非原生支持的語法功能和如何在 Babel 和 TypeScript 中運用以支持 ECMAScript 2015 類。最後介紹幾個 V8 原生支持實現類的例子。github
JavaScript 沒有原始類型且一切皆對象。好比,以下字符串:chrome
const name = "SessionStack";
複製代碼
能夠當即調用新建立對象上的不一樣方法:c#
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 中一切皆對象,可是並無使用經典的基於類的繼承。而是使用原型來實現繼承。bash
在 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 代碼。
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 是另外一個流行的框架。它引入了一種編寫 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 是如何原生支持 ES6 類的。如前面文章所討論的那樣,首先解析新語法爲可運行的 JavaScript 代碼並添加到 AST 樹中。類定義的結果即在語法抽象樹中添加一個 ClassLiteral 類型的新節點。
該節點包含了一些信息。首先,它把構造函數當成單獨的函數且包含類屬性集。這些屬性能夠是一個方法,一個 getter, 一個 setter, 一個公共變量或者私有變量。該節點還儲存了指向父類的指針引用,該父類也並儲存了構造函數,屬性集和及父類引用,依次類推。
一旦把新的 ClassLiteral 轉換爲字節碼,再將其轉化爲各類函數和原型。
今日頭條招人啦!發送簡歷到 likun.liyuk@bytedance.com ,便可走快速內推通道,長期有效!國際化PGC部門的JD以下:c.xiumi.us/board/v5/2H…,也可內推其餘部門!
本系列持續更新中,Github 地址請查閱這裏。