摘要:下面是用實(shí)現(xiàn)轉(zhuǎn)成抽象語(yǔ)法樹(shù)如下還支持繼承以下是轉(zhuǎn)換結(jié)果最終的結(jié)果還是代碼,其中包含庫(kù)中的一些函數(shù)??梢允褂眯碌囊子谑褂玫念?lèi)定義,但是它仍然會(huì)創(chuàng)建構(gòu)造函數(shù)和分配原型。
這是專(zhuān)門(mén)探索 JavaScript 及其所構(gòu)建的組件的系列文章的第 15 篇。
想閱讀更多優(yōu)質(zhì)文章請(qǐng)猛戳GitHub博客,一年百來(lái)篇優(yōu)質(zhì)文章等著你!
如果你錯(cuò)過(guò)了前面的章節(jié),可以在這里找到它們:
JavaScript 是如何工作的:引擎,運(yùn)行時(shí)和調(diào)用堆棧的概述!
JavaScript 是如何工作的:深入V8引擎&編寫(xiě)優(yōu)化代碼的5個(gè)技巧!
JavaScript 是如何工作的:內(nèi)存管理+如何處理4個(gè)常見(jiàn)的內(nèi)存泄漏 !
JavaScript 是如何工作的:事件循環(huán)和異步編程的崛起+ 5種使用 async/await 更好地編碼方式!
JavaScript 是如何工作的:深入探索 websocket 和HTTP/2與SSE +如何選擇正確的路徑!
JavaScript 是如何工作的:與 WebAssembly比較 及其使用場(chǎng)景 !
JavaScript 是如何工作的:Web Workers的構(gòu)建塊+ 5個(gè)使用他們的場(chǎng)景!
JavaScript 是如何工作的:Service Worker 的生命周期及使用場(chǎng)景!
JavaScript 是如何工作的:Web 推送通知的機(jī)制!
JavaScript是如何工作的:使用 MutationObserver 跟蹤 DOM 的變化!
JavaScript是如何工作的:渲染引擎和優(yōu)化其性能的技巧!
JavaScript是如何工作的:深入網(wǎng)絡(luò)層 + 如何優(yōu)化性能和安全!
JavaScript是如何工作的:CSS 和 JS 動(dòng)畫(huà)底層原理及如何優(yōu)化它們的性能!
JavaScript是如何工作的:解析、抽象語(yǔ)法樹(shù)(AST)+ 提升編譯速度5個(gè)技巧!
現(xiàn)在構(gòu)建任何類(lèi)型的軟件項(xiàng)目最流行的方法這是使用類(lèi)。在這篇文章中,探討用 JavaScript 實(shí)現(xiàn)類(lèi)的不同方法,以及如何構(gòu)建類(lèi)的結(jié)構(gòu)。首先從深入研究原型工作原理,并分析在流行庫(kù)中模擬基于類(lèi)的繼承的方法。 接下來(lái)是講如何將新的語(yǔ)法轉(zhuǎn)制為瀏覽器識(shí)別的語(yǔ)法,以及在 Babel 和 TypeScript 中使用它來(lái)引入ECMAScript 2015類(lèi)的支持。最后,將以一些在 V8 中如何本機(jī)實(shí)現(xiàn)類(lèi)的示例來(lái)結(jié)束本文。
概述在 JavaScript 中,沒(méi)有基本類(lèi)型,創(chuàng)建的所有東西都是對(duì)象。例如,創(chuàng)建一個(gè)新字符串:
const name = "SessionStack";
接著在新創(chuàng)建的對(duì)象上調(diào)用不同的方法:
console.log(a.repeat(2)); // SessionStackSessionStack console.log(a.toLowerCase()); // sessionstack
與其他語(yǔ)言不同,在 JavaScript 中,字符串或數(shù)字的聲明會(huì)自動(dòng)創(chuàng)建一個(gè)封裝值的對(duì)象,并提供不同的方法,甚至可以在基本類(lèi)型上執(zhí)行這些方法。
另一個(gè)有趣的事實(shí)是,數(shù)組等復(fù)雜類(lèi)型也是對(duì)象。如果檢查數(shù)組實(shí)例的類(lèi)型,你將看到它是一個(gè)對(duì)象。列表中每個(gè)元素的索引只是對(duì)象中的屬性。當(dāng)通過(guò)數(shù)組中的索引訪問(wèn)一個(gè)元素時(shí),實(shí)際上是訪問(wèn)了數(shù)組對(duì)象的一個(gè) key 值,并得到 key 對(duì)應(yīng)的值。從數(shù)據(jù)的存儲(chǔ)方式看時(shí),這兩個(gè)定義是相同的:
let names = [“SessionStack”]; let names = { “0”: “SessionStack”, “l(fā)ength”: 1 }
因此,訪問(wèn)數(shù)組中的元素和對(duì)象的屬性耗時(shí)是相同的。我(本文作者)通過(guò)多次的努力才發(fā)現(xiàn)這一點(diǎn)的。就是不久,我(本文作者)不得不對(duì)項(xiàng)目中的一段關(guān)鍵代碼進(jìn)行大規(guī)模優(yōu)化。在嘗試了所有簡(jiǎn)單的可選項(xiàng)之后,最后用數(shù)組替換了項(xiàng)目中使用的所有對(duì)象。理論上,訪問(wèn)數(shù)組中的元素比訪問(wèn)哈希映射中的鍵要快且對(duì)性能沒(méi)有任何影響。在 JavaScript中,這兩種操作都是作為訪問(wèn)哈希映射中的鍵來(lái)實(shí)現(xiàn)的,并且花費(fèi)相同的時(shí)間。
使用原型模擬類(lèi)一般的想到對(duì)象時(shí),首先想到的是類(lèi)。我們大都習(xí)慣于根據(jù)類(lèi)及其之間的關(guān)系來(lái)構(gòu)建應(yīng)用程序。盡管 JavaScript 中的對(duì)象無(wú)處不在,但該語(yǔ)言并不使用傳統(tǒng)的基于類(lèi)的繼承,相反,它依賴(lài)于原型來(lái)實(shí)現(xiàn)。
在 JavaScript 中,每個(gè)對(duì)象通過(guò)原型連接著另一個(gè)對(duì)象。當(dāng)嘗試訪問(wèn)對(duì)象上的屬性或方法時(shí),首先從對(duì)象本身開(kāi)始查找,如果沒(méi)有找到任何內(nèi)容,則在對(duì)象的原型中繼續(xù)查找。
從一個(gè)簡(jiǎn)單的例子開(kāi)始:
function Component(content) { this.content = content; } Component.prototype.render = function() { console.log(this.content); }
在 Component 的原型上添加 render 方法,因?yàn)橄M?Component 的每個(gè)實(shí)例都能有 render 方法。Component 任何實(shí)例調(diào)用此方法時(shí),首先將在實(shí)例本身中執(zhí)行查找,如果沒(méi)有,接著從它的原型中執(zhí)行查找。
接著引入一個(gè)新的子類(lèi):
function InputField(value) { this.content = ``; }
如果想要 InputField 繼承 Component 并能夠調(diào)用它的 render 方法,就需要更改它的原型。當(dāng)對(duì)子類(lèi)的實(shí)例調(diào)用 render 方法時(shí),不希望在它的空原型中查找,而應(yīng)該從從 Component 上的原型查找:
InputField.prototype = Object.create(new Component());
通過(guò)這種方式,就可以在 Component 的原型中找到 render 方法。為了實(shí)現(xiàn)繼承,需要將 InputField 的原型連接到 Component 的實(shí)例上,大多數(shù)庫(kù)都使用 Object.setPrototypeOf 方法來(lái)實(shí)現(xiàn)這一點(diǎn)。
然而,這不是唯一一件事要做的,每次繼承一個(gè)類(lèi),需要:
將子類(lèi)的原型指向父類(lèi)的實(shí)例。
在子類(lèi)構(gòu)造函數(shù)中調(diào)用的父構(gòu)造函數(shù),完成父構(gòu)造函數(shù)中的初始化邏輯。
如上所述,如果希望繼承基類(lèi)的的所有特性,那么每次都需要執(zhí)行這個(gè)復(fù)雜的邏輯。當(dāng)創(chuàng)建多個(gè)類(lèi)時(shí),將邏輯封裝在可重用函數(shù)中是有意義的。這就是開(kāi)發(fā)人員最初解決基于類(lèi)繼承的方法——通過(guò)使用不同的庫(kù)來(lái)模擬它。
這些解決方案越來(lái)越流行,造成了 JS 中明顯缺少了一些類(lèi)型的現(xiàn)象。這就是為什么在 ECMAScript 2015 的第一個(gè)主要版本中引入了類(lèi),繼承的新語(yǔ)法。
類(lèi)的轉(zhuǎn)換當(dāng) ES6 或 ECMAScript 2015 中的新特性被提出時(shí),JavaScript 開(kāi)發(fā)人員不能等待所有引擎和瀏覽器都開(kāi)始支持它們。為實(shí)現(xiàn)瀏覽器能夠支持新的特性一個(gè)好方法是通過(guò) 轉(zhuǎn)換 (Transpiling) ,它允許將 ECMAScript 2015 中編寫(xiě)的代碼轉(zhuǎn)換成任何瀏覽器都能理解的 JavaScript 代碼,當(dāng)然也包括使用基于類(lèi)的繼承編寫(xiě)類(lèi)的轉(zhuǎn)換功能。
Babel最流行的 JavaScript 編譯器之一就是 Babel,宏觀來(lái)說(shuō),它分3個(gè)階段運(yùn)行代碼:解析(parsing),轉(zhuǎn)譯(transforming),生成(generation),來(lái)看看它是如何轉(zhuǎn)換的:
class Component { constructor(content) { this.content = content; } render() { console.log(this.content) } } const component = new Component("SessionStack"); component.render();
以下是 Babel 轉(zhuǎn)換后的樣式:
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; }();
如上所見(jiàn),轉(zhuǎn)換后的代碼就可在任何瀏覽器執(zhí)行了。 此外,還添加了一些功能, 這些是 Babel 標(biāo)準(zhǔn)庫(kù)的一部分。
_classCallCheck 和_createClass 作為函數(shù)包含在編譯文件中。
_classCallCheck 函數(shù)的作用在于確保構(gòu)造方法永遠(yuǎn)不會(huì)作為函數(shù)被調(diào)用,它會(huì)評(píng)估函數(shù)的上下文是否為 Component 對(duì)象的實(shí)例,以此確定是否需要拋出異常。
_createClass 用于處理創(chuàng)建對(duì)象屬性,函數(shù)支持傳入構(gòu)造函數(shù)與需定義的鍵值對(duì)屬性數(shù)組。函數(shù)判斷傳入的參數(shù)(普通方法/靜態(tài)方法)是否為空對(duì)應(yīng)到不同的處理流程上。
為了探究繼承的實(shí)現(xiàn)原理,分析繼承的 Component 的 InputField 類(lèi)。。
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 創(chuàng)建了 _inherits 函數(shù)幫助實(shí)現(xiàn)繼承。
以 ES6 轉(zhuǎn) ES5 為例,具體過(guò)程:
編寫(xiě)ES6代碼
babylon 進(jìn)行解析
解析得到 AST
plugin 用 babel-traverse 對(duì) AST 樹(shù)進(jìn)行遍歷轉(zhuǎn)譯
得到新的 AST樹(shù)
用 babel-generator 通過(guò) AST 樹(shù)生成 ES5 代碼
Babel 中的抽象語(yǔ)法樹(shù)AST 包含多個(gè)節(jié)點(diǎn),且每個(gè)節(jié)點(diǎn)只有一個(gè)父節(jié)點(diǎn)。 在 Babel 中,每個(gè)形狀樹(shù)的節(jié)點(diǎn)包含可視化類(lèi)型、位置、在樹(shù)中的連接等信息。 有不同類(lèi)型的節(jié)點(diǎn),如 string,numbers,null等,還有用于流控制(if)和循環(huán)(for,while)的語(yǔ)句節(jié)點(diǎn)。 并且還有一種特殊類(lèi)型的節(jié)點(diǎn)用于類(lèi)。它是基節(jié)點(diǎn)類(lèi)的一個(gè)子節(jié)點(diǎn),通過(guò)添加字段來(lái)擴(kuò)展它,以存儲(chǔ)對(duì)基類(lèi)的引用和作為多帶帶節(jié)點(diǎn)的類(lèi)的主體。
把下面的代碼片段轉(zhuǎn)換成一個(gè)抽象語(yǔ)法樹(shù):
class Component { constructor(content) { this.content = content; } render() { console.log(this.content) } }
下面是以下代碼片段的抽象語(yǔ)法樹(shù):
Babel 的三個(gè)主要處理步驟分別是: 解析(parse),轉(zhuǎn)換 (transform),生成 (generate)。
解析將代碼解析成抽象語(yǔ)法樹(shù)(AST),每個(gè)js引擎(比如Chrome瀏覽器中的V8引擎)都有自己的AST解析器,而B(niǎo)abel是通過(guò) Babylon 實(shí)現(xiàn)的。在解析過(guò)程中有兩個(gè)階段: 詞法分析 和 語(yǔ)法分析 ,詞法分析階段把字符串形式的代碼轉(zhuǎn)換為 令牌 (tokens)流,令牌類(lèi)似于AST中節(jié)點(diǎn);而語(yǔ)法分析階段則會(huì)把一個(gè)令牌流轉(zhuǎn)換成 AST的形式,同時(shí)這個(gè)階段會(huì)把令牌中的信息轉(zhuǎn)換成AST的表述結(jié)構(gòu)。
轉(zhuǎn)換在這個(gè)階段,Babel接受得到AST并通過(guò)babel-traverse對(duì)其進(jìn)行 深度優(yōu)先遍歷,在此過(guò)程中對(duì)節(jié)點(diǎn)進(jìn)行添加、更新及移除操作。這部分也是Babel插件介入工作的部分。
生成將經(jīng)過(guò)轉(zhuǎn)換的AST通過(guò)babel-generator再轉(zhuǎn)換成js代碼,過(guò)程就是 深度優(yōu)先遍歷整個(gè)AST,然后構(gòu)建可以表示轉(zhuǎn)換后代碼的字符串。
在上面的示例中,首先生成兩個(gè) MethodDefinition 節(jié)點(diǎn)的代碼,然后生成類(lèi)主體節(jié)點(diǎn)的代碼,最后生成類(lèi)聲明節(jié)點(diǎn)的代碼。
使用 TypeScript 進(jìn)行轉(zhuǎn)換另一個(gè)利用轉(zhuǎn)換的流行框架是 TypeScript。它引入了一種用于編寫(xiě) JavaScript 應(yīng)用程序的新語(yǔ)法,該語(yǔ)法被轉(zhuǎn)換為任何瀏覽器或引擎都可以執(zhí)行的 EMCAScript 5。下面是用 Typescript 實(shí)現(xiàn) Component :
class Component { content: string; constructor(content: string) { this.content = content; } render() { console.log(this.content) } }
轉(zhuǎn)成抽象語(yǔ)法樹(shù)如下:
Typescript 還支持繼承:
class InputField extends Component { constructor(value: string) { const content = ``; super(content); } }
以下是轉(zhuǎn)換結(jié)果:
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));
最終的結(jié)果還是 ECMAScript 5 代碼,其中包含 TypeScript 庫(kù)中的一些函數(shù)。封 __extends 中的邏輯與在第一節(jié)中討論的邏輯相同。
隨著 Babel 和 TypeScript 被廣泛采用,標(biāo)準(zhǔn)類(lèi)和基于類(lèi)的繼承成為了構(gòu)造 JavaScript 應(yīng)用程序的標(biāo)準(zhǔn)方式,這推動(dòng)了在瀏覽器中引入對(duì)類(lèi)的原生支持。
類(lèi)的原生支持2014年,Chrome 引入了對(duì) 類(lèi)的原生支持,這允許在不需要任何庫(kù)或轉(zhuǎn)換器的情況下執(zhí)行類(lèi)聲明語(yǔ)法。
本地實(shí)現(xiàn)類(lèi)的過(guò)程就是我們所說(shuō)的語(yǔ)法糖。這只是一種奇特的語(yǔ)法,它可以編譯成語(yǔ)言中已經(jīng)支持的相同的原語(yǔ)。可以使用新的易于使用的類(lèi)定義,但是它仍然會(huì)創(chuàng)建構(gòu)造函數(shù)和分配原型。
V8的支持撯著,看看在 V8 中對(duì) ECMAScript 2015 類(lèi)的本機(jī)支持的工作原理。正如在 前一篇文章 中所討論的,首先必須將新語(yǔ)法解析為有效的 JavaScript 代碼并添加到 AST 中,因此,作為類(lèi)定義的結(jié)果,一個(gè)具有ClassLiteral 類(lèi)型的新節(jié)點(diǎn)被添加到樹(shù)中。
這個(gè)節(jié)點(diǎn)存儲(chǔ)了一些信息。首先,它將構(gòu)造函數(shù)作為一個(gè)多帶帶的函數(shù)保存,還保存類(lèi)屬性的列表,這些屬性包括 方法、getter、setter、公共字段或私有字段。該節(jié)點(diǎn)還存儲(chǔ)對(duì)父類(lèi)的引用,該類(lèi)將繼承父類(lèi),而父類(lèi)將再次存儲(chǔ)構(gòu)造函數(shù)、屬性列表和父類(lèi)。
一旦這個(gè)新的類(lèi) ClassLiteral 被 轉(zhuǎn)換成代碼,它又被轉(zhuǎn)換成函數(shù)和原型。
原文:
https://blog.sessionstack.com...
代碼部署后可能存在的BUG沒(méi)法實(shí)時(shí)知道,事后為了解決這些BUG,花了大量的時(shí)間進(jìn)行l(wèi)og 調(diào)試,這邊順便給大家推薦一個(gè)好用的BUG監(jiān)控工具 Fundebug。
你的點(diǎn)贊是我持續(xù)分享好東西的動(dòng)力,歡迎點(diǎn)贊!
交流干貨系列文章匯總?cè)缦?,覺(jué)得不錯(cuò)點(diǎn)個(gè)Star,歡迎 加群 互相學(xué)習(xí)。
https://github.com/qq44924588...
我是小智,公眾號(hào)「大遷世界」作者,對(duì)前端技術(shù)保持學(xué)習(xí)愛(ài)好者。我會(huì)經(jīng)常分享自己所學(xué)所看的干貨,在進(jìn)階的路上,共勉!
關(guān)注公眾號(hào),后臺(tái)回復(fù)福利,即可看到福利,你懂的。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/101334.html
摘要:使用新的易用的類(lèi)定義,歸根結(jié)底也是要?jiǎng)?chuàng)建構(gòu)造函數(shù)和修改原型。首先,它把構(gòu)造函數(shù)當(dāng)成單獨(dú)的函數(shù)且包含類(lèi)屬性集。該節(jié)點(diǎn)還儲(chǔ)存了指向父類(lèi)的指針引用,該父類(lèi)也并儲(chǔ)存了構(gòu)造函數(shù),屬性集和及父類(lèi)引用,依次類(lèi)推。 原文請(qǐng)查閱這里,略有刪減,本文采用知識(shí)共享署名 4.0 國(guó)際許可協(xié)議共享,BY Troland。 本系列持續(xù)更新中,Github 地址請(qǐng)查閱這里。 這是 JavaScript 工作原理的第...
摘要:使用新的易用的類(lèi)定義,歸根結(jié)底也是要?jiǎng)?chuàng)建構(gòu)造函數(shù)和修改原型。首先,它把構(gòu)造函數(shù)當(dāng)成單獨(dú)的函數(shù)且包含類(lèi)屬性集。該節(jié)點(diǎn)還儲(chǔ)存了指向父類(lèi)的指針引用,該父類(lèi)也并儲(chǔ)存了構(gòu)造函數(shù),屬性集和及父類(lèi)引用,依次類(lèi)推。 原文請(qǐng)查閱這里,略有刪減,本文采用知識(shí)共享署名 4.0 國(guó)際許可協(xié)議共享,BY Troland。 本系列持續(xù)更新中,Github 地址請(qǐng)查閱這里。 這是 JavaScript 工作原理的第...
摘要:為了方便大家共同學(xué)習(xí),整理了之前博客系列的文章,目前已整理是如何工作這個(gè)系列,可以請(qǐng)猛戳博客查看。以下列出該系列目錄,歡迎點(diǎn)個(gè)星星,我將更友動(dòng)力整理理優(yōu)質(zhì)的文章,一起學(xué)習(xí)。 為了方便大家共同學(xué)習(xí),整理了之前博客系列的文章,目前已整理 JavaScript 是如何工作這個(gè)系列,可以請(qǐng)猛戳GitHub博客查看。 以下列出該系列目錄,歡迎點(diǎn)個(gè)星星,我將更友動(dòng)力整理理優(yōu)質(zhì)的文章,一起學(xué)習(xí)。 J...
摘要:在他的重學(xué)前端課程中提到到現(xiàn)在為止,前端工程師已經(jīng)成為研發(fā)體系中的重要崗位之一。大部分前端工程師的知識(shí),其實(shí)都是來(lái)自于實(shí)踐和工作中零散的學(xué)習(xí)。一基礎(chǔ)前端工程師吃飯的家伙,深度廣度一樣都不能差。 開(kāi)篇 前端開(kāi)發(fā)是一個(gè)非常特殊的行業(yè),它的歷史實(shí)際上不是很長(zhǎng),但是知識(shí)之繁雜,技術(shù)迭代速度之快是其他技術(shù)所不能比擬的。 winter在他的《重學(xué)前端》課程中提到: 到現(xiàn)在為止,前端工程師已經(jīng)成為研...
摘要:在他的重學(xué)前端課程中提到到現(xiàn)在為止,前端工程師已經(jīng)成為研發(fā)體系中的重要崗位之一。大部分前端工程師的知識(shí),其實(shí)都是來(lái)自于實(shí)踐和工作中零散的學(xué)習(xí)。一基礎(chǔ)前端工程師吃飯的家伙,深度廣度一樣都不能差。開(kāi)篇 前端開(kāi)發(fā)是一個(gè)非常特殊的行業(yè),它的歷史實(shí)際上不是很長(zhǎng),但是知識(shí)之繁雜,技術(shù)迭代速度之快是其他技術(shù)所不能比擬的。 winter在他的《重學(xué)前端》課程中提到: 到現(xiàn)在為止,前端工程師已經(jīng)成為研發(fā)體系...
閱讀 2480·2021-10-12 10:11
閱讀 1229·2021-10-11 10:58
閱讀 3273·2019-08-30 15:54
閱讀 712·2019-08-30 13:59
閱讀 680·2019-08-29 13:07
閱讀 1407·2019-08-26 11:55
閱讀 2142·2019-08-26 10:44
閱讀 2642·2019-08-23 18:25