摘要:下面是用實現轉成抽象語法樹如下還支持繼承以下是轉換結果最終的結果還是代碼,其中包含庫中的一些函數。可以使用新的易于使用的類定義,但是它仍然會創建構造函數和分配原型。
這是專門探索 JavaScript 及其所構建的組件的系列文章的第 15 篇。
想閱讀更多優質文章請猛戳GitHub博客,一年百來篇優質文章等著你!
如果你錯過了前面的章節,可以在這里找到它們:
JavaScript 是如何工作的:引擎,運行時和調用堆棧的概述!
JavaScript 是如何工作的:深入V8引擎&編寫優化代碼的5個技巧!
JavaScript 是如何工作的:內存管理+如何處理4個常見的內存泄漏 !
JavaScript 是如何工作的:事件循環和異步編程的崛起+ 5種使用 async/await 更好地編碼方式!
JavaScript 是如何工作的:深入探索 websocket 和HTTP/2與SSE +如何選擇正確的路徑!
JavaScript 是如何工作的:與 WebAssembly比較 及其使用場景 !
JavaScript 是如何工作的:Web Workers的構建塊+ 5個使用他們的場景!
JavaScript 是如何工作的:Service Worker 的生命周期及使用場景!
JavaScript 是如何工作的:Web 推送通知的機制!
JavaScript是如何工作的:使用 MutationObserver 跟蹤 DOM 的變化!
JavaScript是如何工作的:渲染引擎和優化其性能的技巧!
JavaScript是如何工作的:深入網絡層 + 如何優化性能和安全!
JavaScript是如何工作的:CSS 和 JS 動畫底層原理及如何優化它們的性能!
JavaScript是如何工作的:解析、抽象語法樹(AST)+ 提升編譯速度5個技巧!
現在構建任何類型的軟件項目最流行的方法這是使用類。在這篇文章中,探討用 JavaScript 實現類的不同方法,以及如何構建類的結構。首先從深入研究原型工作原理,并分析在流行庫中模擬基于類的繼承的方法。 接下來是講如何將新的語法轉制為瀏覽器識別的語法,以及在 Babel 和 TypeScript 中使用它來引入ECMAScript 2015類的支持。最后,將以一些在 V8 中如何本機實現類的示例來結束本文。
概述在 JavaScript 中,沒有基本類型,創建的所有東西都是對象。例如,創建一個新字符串:
const name = "SessionStack";
接著在新創建的對象上調用不同的方法:
console.log(a.repeat(2)); // SessionStackSessionStack console.log(a.toLowerCase()); // sessionstack
與其他語言不同,在 JavaScript 中,字符串或數字的聲明會自動創建一個封裝值的對象,并提供不同的方法,甚至可以在基本類型上執行這些方法。
另一個有趣的事實是,數組等復雜類型也是對象。如果檢查數組實例的類型,你將看到它是一個對象。列表中每個元素的索引只是對象中的屬性。當通過數組中的索引訪問一個元素時,實際上是訪問了數組對象的一個 key 值,并得到 key 對應的值。從數據的存儲方式看時,這兩個定義是相同的:
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); }
在 Component 的原型上添加 render 方法,因為希望 Component 的每個實例都能有 render 方法。Component 任何實例調用此方法時,首先將在實例本身中執行查找,如果沒有,接著從它的原型中執行查找。
接著引入一個新的子類:
function InputField(value) { this.content = ``; }
如果想要 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 用于處理創建對象屬性,函數支持傳入構造函數與需定義的鍵值對屬性數組。函數判斷傳入的參數(普通方法/靜態方法)是否為空對應到不同的處理流程上。
為了探究繼承的實現原理,分析繼承的 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);
在本例中, Babel 創建了 _inherits 函數幫助實現繼承。
以 ES6 轉 ES5 為例,具體過程:
編寫ES6代碼
babylon 進行解析
解析得到 AST
plugin 用 babel-traverse 對 AST 樹進行遍歷轉譯
得到新的 AST樹
用 babel-generator 通過 AST 樹生成 ES5 代碼
Babel 中的抽象語法樹AST 包含多個節點,且每個節點只有一個父節點。 在 Babel 中,每個形狀樹的節點包含可視化類型、位置、在樹中的連接等信息。 有不同類型的節點,如 string,numbers,null等,還有用于流控制(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 = ``; 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));
最終的結果還是 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...
我是小智,公眾號「大遷世界」作者,對前端技術保持學習愛好者。我會經常分享自己所學所看的干貨,在進階的路上,共勉!
關注公眾號,后臺回復福利,即可看到福利,你懂的。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/101334.html
摘要:使用新的易用的類定義,歸根結底也是要創建構造函數和修改原型。首先,它把構造函數當成單獨的函數且包含類屬性集。該節點還儲存了指向父類的指針引用,該父類也并儲存了構造函數,屬性集和及父類引用,依次類推。 原文請查閱這里,略有刪減,本文采用知識共享署名 4.0 國際許可協議共享,BY Troland。 本系列持續更新中,Github 地址請查閱這里。 這是 JavaScript 工作原理的第...
摘要:使用新的易用的類定義,歸根結底也是要創建構造函數和修改原型。首先,它把構造函數當成單獨的函數且包含類屬性集。該節點還儲存了指向父類的指針引用,該父類也并儲存了構造函數,屬性集和及父類引用,依次類推。 原文請查閱這里,略有刪減,本文采用知識共享署名 4.0 國際許可協議共享,BY Troland。 本系列持續更新中,Github 地址請查閱這里。 這是 JavaScript 工作原理的第...
摘要:為了方便大家共同學習,整理了之前博客系列的文章,目前已整理是如何工作這個系列,可以請猛戳博客查看。以下列出該系列目錄,歡迎點個星星,我將更友動力整理理優質的文章,一起學習。 為了方便大家共同學習,整理了之前博客系列的文章,目前已整理 JavaScript 是如何工作這個系列,可以請猛戳GitHub博客查看。 以下列出該系列目錄,歡迎點個星星,我將更友動力整理理優質的文章,一起學習。 J...
摘要:在他的重學前端課程中提到到現在為止,前端工程師已經成為研發體系中的重要崗位之一。大部分前端工程師的知識,其實都是來自于實踐和工作中零散的學習。一基礎前端工程師吃飯的家伙,深度廣度一樣都不能差。 開篇 前端開發是一個非常特殊的行業,它的歷史實際上不是很長,但是知識之繁雜,技術迭代速度之快是其他技術所不能比擬的。 winter在他的《重學前端》課程中提到: 到現在為止,前端工程師已經成為研...
摘要:在他的重學前端課程中提到到現在為止,前端工程師已經成為研發體系中的重要崗位之一。大部分前端工程師的知識,其實都是來自于實踐和工作中零散的學習。一基礎前端工程師吃飯的家伙,深度廣度一樣都不能差。開篇 前端開發是一個非常特殊的行業,它的歷史實際上不是很長,但是知識之繁雜,技術迭代速度之快是其他技術所不能比擬的。 winter在他的《重學前端》課程中提到: 到現在為止,前端工程師已經成為研發體系...
閱讀 2475·2021-10-12 10:11
閱讀 1223·2021-10-11 10:58
閱讀 3263·2019-08-30 15:54
閱讀 704·2019-08-30 13:59
閱讀 674·2019-08-29 13:07
閱讀 1398·2019-08-26 11:55
閱讀 2138·2019-08-26 10:44
閱讀 2635·2019-08-23 18:25