象語法樹(Abstract Syntax Trees),簡稱AST,如果您正在編寫代碼,那么 AST 很可能已經參與了您的開發流程。它們為您的開發流程的許多部分提供動力。 有些人可能在編譯器的上下文中聽說過它們,但它們被用于各種工具中。 即使您不編寫通用開發工具,AST 也可能是您工具帶中的有用工具。 在這篇文章中,我們將討論什么是 AST,它們在哪里使用以及如何利用它們。
什么是AST
抽象語法樹或 AST 是代碼的樹型數據結構表示。 它們是編譯器工作方式的基本部分。 當編譯器轉換某些代碼時,基本上有以下步驟:
詞法分析又名標記化
在此步驟中,您編寫的代碼將被轉換為一組描述代碼不同部分的標記。 這與基本語法突出顯示使用的方法基本相同。 這些標記令牌不了解事物如何組合在一起,并且僅關注文件的組件。
你可以想象這就像你將一個文本分解成單詞。 您可能能夠區分標點符號、動詞、名詞、數字等,但在這個階段,您對句子的組成部分或句子如何組合沒有任何更深入的了解。
語法分析又名解析
這是我們將標記列表轉換為抽象語法樹的步驟。 它將我們的標記轉換為表示代碼實際結構的樹。 以前在標記中我們只有一對 (),現在我們知道它是函數調用、函數定義、分組還是其他東西。
這里的等價物是將我們的單詞列表轉換為表示諸如句子之類的數據結構,某個名詞在句子中扮演什么角色,或者我們是否在列表中。
另一個可以與之比較的例子是 DOM。 上一步只是將 HTML 分解為“標簽”和“文本”,而這一步將生成表示為 DOM 樹的層次結構。
需要注意的一件事是沒有“單一”的 AST 格式。 它們可能會有所不同,這取決于您要轉換為 AST 的語言以及您用于解析的工具。 在 JavaScript 中,一個通用標準是 ESTree,但您會看到不同的工具可能會添加不同的屬性。
一般來說,AST 是一種樹結構,其中每個節點至少有一個類型來指定它所代表的內容。
代碼生成
此步驟本身可以是多個步驟。 一旦我們有了抽象語法樹,我們就可以操作它,也可以將它“打印”到不同類型的代碼中。 使用 AST 操作代碼比直接在代碼上作為文本或標記列表執行這些操作更安全。
操縱文本總是很危險的; 它顯示最少的上下文。 如果您曾經嘗試使用字符串替換或正則表達式來操作文本,您可能會注意到很容易出錯。而且不容易調試。
甚至操縱令牌也不容易。 雖然我們可能知道變量是什么,但如果我們想重命名它,我們將無法深入了解變量的范圍或可能與之沖突的任何變量。
AST 提供了有關代碼結構的足夠信息,我們可以更有信心地對其進行修改。 例如,我們可以確定變量的聲明位置,并確切地知道由于樹結構而影響程序的哪個部分。
一旦我們操縱了樹,我們就可以打印樹以輸出任何預期的代碼輸出。 例如,如果我們要構建一個像 TypeScript 編譯器這樣的編譯器,我們會輸出 JavaScript,而另一個編譯器可能會輸出機器代碼。
同樣,使用 AST 更容易實現這一點,因為相同結構的不同輸出可能具有不同的格式。 使用更線性的輸入(如文本或標記列表)生成輸出會相當困難。
如何處理 AST?
理論涵蓋了哪些實際生活中的 AST 用例? 我們討論了編譯器,但我們并不是整天都在構建編譯器。
AST 的用例很廣泛,通常可以分為三個總體操作:讀取、修改和打印。 它們是一種添加劑,這意味著如果您正在打印 AST,那么您以前也閱讀過 AST 并對其進行修改的可能性很高。 但我們將介紹每個主要關注一個用例的示例。
讀取/遍歷 AST
從技術上講,使用 AST 的第一步是解析文本以創建 AST,但在大多數情況下,提供解析步驟的庫也提供了一種遍歷 AST 的方法。遍歷 AST 意味著訪問樹的不同節點以獲取細節或執行操作。
最常見的用例之一是 linting。 例如,ESLint 使用 espree 生成 AST,如果您想編寫任何自定義規則,您將根據不同的 AST 節點編寫這些規則。 ESLint 文檔有大量關于如何構建自定義規則、插件和格式化程序的文檔。
修改/轉換 AST
如前所述,與將代碼修改為標記或原始字符串相比,擁有 AST 使修改所述樹更容易、更安全。您可能想要使用 AST 修改某些代碼的原因有很多種。
例如,Babel 修改 AST 以向下編譯新功能或將 JSX 轉換為函數調用。例如,當您編譯 React 或 Preact 代碼時會發生這種情況。
另一個用例是捆綁代碼。在模塊的世界中,捆綁代碼通常比將文件附加在一起要復雜得多。更好地了解各個文件的結構可以更輕松地合并這些文件并在必要時調整導入和函數調用。如果您查看 webpack、parcel 或 rollup 等工具的代碼庫,您會發現它們都使用 AST 作為其捆綁工作流程的一部分。
打印 AST
在大多數情況下,打印和修改 AST 是齊頭并進的,因為您必須輸出剛剛修改的 AST。 但是,雖然像 recast 這樣的一些庫明確專注于以與原始代碼樣式相同的代碼樣式打印 AST,但也有各種用例,您希望以不同的方式顯式打印您的 AST。
例如,Prettier 使用 AST 根據您的配置重新格式化您的代碼,而無需更改代碼的內容/含義。 他們這樣做的方式是將您的代碼轉換為完全與格式無關的 AST,然后根據您的規則重寫它。
其他常見的用例是用不同的目標語言打印代碼或構建自己的縮小工具。
您可以使用幾種不同的工具來打印 AST,例如 escodegen 或 astring。 您還可以根據您的用例構建自己的格式化程序,或者為 Prettier 構建一個插件。
最后:
雖然 AST 可能是大多數開發人員每天都不會使用的東西,但我相信了解它對今后的工作會有幫助。感謝閱讀。
一節聊到正則表達式的簡單應用,不足之處歡迎留言交流。
Javascript正則表達式示例之基本概念
今天,我們來看一下,如何使用正則表達式,匹配HTML標簽及相關信息。
為什么要加上相關信息呢?
因為,如果您想寫一個HTML語法樹解析庫的時候,可能會用到。
下面內容用到的語法
|:表示或者,要么前面,要么后面
(?<=我前面出現的內容)要匹配的內容:只匹配前面出現的字符之后的內容。
可視圖
要匹配的內容(?=我前面出現的內容):只匹配后面出現的字符之前的內容。
可視圖
分組捕獲:一對完整的小括號(),表示一個組。
\數字:你要使用那一個分組捕獲到的內容。
.*?:在正則表達式中,. 表示匹配任意字符,* 表示匹配 0 到任意次的前一個字符,? 表示非貪婪匹配,即盡可能匹配最少的字符。因此,.*? 表示匹配任意字符零次或多次,但盡可能匹配最少的字符。這個表達式通常用于匹配一個字符串中的所有內容,但是避免貪婪匹配導致的匹配錯誤。
^: 表示匹配開始
[要匹配的字符]:只匹配括號中的字符。
比如[0-9]、[a-z]、[A-Z]、[0-9a-zA-Z]、[0-9abc]等等。
[^要匹配的字符]:[]中加^表示匹配不是“要匹配的字符”。
<body><div id="left">left</div><div id="right">right</div></body>
const text = document.body.innerText;
text = text.replace(/\n/g, '');
console.log(text);
//輸出: leftright
假設沒有innerText的功能呢?實現這個功能,使用正則表達式無疑是最方便的。
var text = document.body.innerHTML.replace(/<[^>]+>/g,'');
text = text.replace(/\n/g, '');
console.log(text);
//輸出: leftright
匹配結果
可視圖
是的,這個正則表達式的意思是,查找<>并且包含他們之間不為>的一段字符串。
到這里,您以為就結束了嗎?您在網上搜索匹配HTML標簽,可能也會得到這么一個結果(例如:<[^>]+>、<.*?>、等等),但實際上這只是開始,我們本著只要是程序就可能有bug的原則,所以我們來看下面一個例子。
const strHtml = '<span data-code=">">>是大于符號。</span>';
const strRes = strHtml.replace(/<[^>]+>/g, '');
console.log(strRes);
// ">>是大于符號。
[可憐]bug出現了,怎么辦?別著急,請看下一個知識點。
2.1、首先,我們先解決第一點最后的bug。
const strHtml = '<span data-code=">">>是大于符號。</span>';
// 一個小改動即可。
const strRes = strHtml.replace(/<("[^"]*"|[^>])+>/g, '');
console.log(strRes);
// >是大于符號。
可視圖
完美[打臉] ,還沒結束……
const strHtml = "<span data-code='>'>>是大于符號。</span>";
const strRes = strHtml.replace(/<("[^"]*"|[^>])+>/g, '');
console.log(strRes);
// '>>是大于符號。
甲:這不是我寫的HTML不標準,是你的解析庫兼容性不好,瀏覽器都可以識別,你為什么不可以?
已:……。
const strHtml = `<i code="<"><小于符號。</i><i code='>'>>大于符號。</i>`;
// 繼續改造
const strRes = strHtml.replace(/<((["'])+.*?\2|[^>])+>/g, '');
console.log(strRes);
// <小于符號。>大于符號。
匹配結果
可視圖
是的,利用正則表達式分組捕獲的語法,實現了上面的需求。
2.2 現在,我們來看看,如何找到某個標簽的所有屬性。
const strHtml = `
<input type='text' disabled value="" class="txt txt-md" v-on:click="save('button')" />
`;
上面的例子中,有多種情況,我們首先來整理出來。
屬性1:type='text'
/[\w]+=(["'])+.*?/
屬性2:disabled
/[\w]+/
屬性3:value=""
/[\w]+=(["'])+.*?/
屬性4:class="txt txt-md"
/[\w]+=(["'])+.*?/
屬性5:v-on:click="save('button')"
/[\w:]+=(["'])+.*?/
其他情況:歡迎討論。
把所有情況連起來之后。
const strHtml = `<input type='text' disabled value="" class="txt txt-md" v-on:click="save('button')" />`;
const tagAttrs = strHtml.match(/(?<=\s)[\w:-]+(=(["']).*?\2)*/g) || [];
console.log(tagAttrs);
// ["type='text'", 'disabled', 'value=""', 'class="txt txt-md"', `v-on:click="save('button')"`]
匹配結果
可視圖
人人為我,我為人人,歡迎您的瀏覽,我們一起加油吧。
產品說要讓前端用 JavaScript 畫一棵樹出來,但是這難道不能直接讓 UI 給一張圖片嗎?
后來一問才知道,產品要的是一顆 隨機樹,也就是樹的茂盛程度、長度、枝干粗細都是隨機的,那這確實沒辦法叫 UI 給圖,畢竟 UI 不可能給我 10000 張樹的圖片吧?
所以第一時間想到的就是 Canvas,用它來畫這棵隨機樹(文末有完整代碼)
接下來使用 Canvas 去畫這棵隨機樹
基礎頁面
我們需要在頁面上寫一個 canvas 標簽,并設置好寬高,同時需要獲取它的 Dom 節點、繪制上下文,以便后續的繪制
坐標調整
默認的 Canvas 坐標系是這樣的
但是我們現在需要從中間去向上去畫一棵樹,所以坐標得調整成這樣:
這些操作可以使用 Canvas 的方法
繪制一棵樹的要素
繪制一棵樹的要素是什么呢?其實就是樹枝和果實,但是其實樹枝才是第一要素,那么樹枝又有哪些要素呢?無非就這幾個點
開始繪制
所以我們可以寫一個 drawBranch 來進行繪制,并且初始調用肯定是繪制樹干,樹干的參數如下:
這個終點應該怎么算呢?其實很簡單,根據樹枝長度、生長角度就可以算出來了,這是初高中的知識
于是我們可以使用 Canvas 的繪制方法,去繪制線段,其實樹枝就是一個一個的線段
到現在我繪制出了一個樹干 出來
但是我們是想讓這棵樹開枝散葉,所以需要繼續遞歸繼續去繪制更多的樹枝出來~
遞歸繪制
其實往哪開枝散葉呢?無非就是往左或者往右
所以需要遞歸畫左邊和右邊的樹枝,并且子樹枝肯定要比父樹枝更短、比父樹枝更細,比如我們可以定義一個比例
而子樹枝的生長角度,其實可以隨機,我們可以在 0° - 30° 之間隨機選一個角度,于是增加了遞歸調用的代碼
但是這個時候會發現,報錯了,爆棧了,因為我們只遞歸開始,但卻沒有在某個時刻遞歸停止
我們可以自己定義一個停止規則(規則可以自己定義,這會決定你這棵樹的茂盛程度):
現在可以看到我們已經大致繪制出一棵樹了
不過還少了樹的果實
繪制果實
繪制果實很簡單,只需要在繪制樹枝結束的時候,去把果實繪制出來就行,其實果實就是一個個的白色實心圓
至此這棵樹完整繪制完畢
繪制部分的代碼如下
*請認真填寫需求信息,我們會在24小時內與您取得聯系。