在上一節介紹了標記的解析,就至關於識別了一句話裏有哪些詞語,接下來就是把這些詞語組成完整的句子,即拼裝標記爲語法樹。html
樹是計算機數據結構裏的專業術語。就像一個學校有不少年級,每一個年級下面有不少班,每一個班級下面有不少學生,這種組織結構就叫樹。node
不少人一提到樹就想起二叉樹,說明你壓根不懂什麼是樹。二叉樹只是樹的一種。二叉樹被用的最多的地方在試卷,請忘掉這個詞。git
從樹中的任一個節點開始,均可以遍歷這個節點的全部後代節點。由於節點不會出現循環關係,因此遍歷樹也不會出現死循環。 github
遍歷節點的順序有有不少,沒特別說明的話,是按照先父節點、再子節點,同級節點則從左到右的順序(圖中編號順序)。typescript
語法樹用於表示解析以後的代碼結構的一種樹。express
好比如下代碼解析後的語法樹如圖:數據結構
var x = ['l', [100]]
if (x) { foo(x) }
其中,源文件(Source File)是語法樹的根節點。app
語法樹中有不少種類的節點,根據種類的不一樣,這些節點的子節點種類也會變化。好比:函數
TypeScript 中,節點有約 100 種,它們都繼承 「Node」 接口:工具
export interface Node extends TextRange {
kind: SyntaxKind;
flags: NodeFlags;
parent: Node;
// ...(略)
}
Node 接口中 kind 枚舉用於標記這個節點的種類。
TypeScript 將表示標記種類的枚舉和表示節點種類的枚舉合併成一個了(這可能也是致使不少人讀不懂代碼的緣由之一):
export const enum SyntaxKind {
// ...(略)
TemplateSpan, SemicolonClassElement, // Element Block, EmptyStatement, VariableStatement, ExpressionStatement, IfStatement, DoStatement, WhileStatement, ForStatement, ForInStatement, ForOfStatement, ContinueStatement, BreakStatement, // ...(略) }
若是你深知「學而不思則罔」的道理,如今應該會思考這樣一個問題:那到底有哪 100 種語法節點呢?
這裏先推薦一個工具:https://astexplorer.net/
這個工具能夠在左側輸入代碼,右側查看實時生成的語法樹(以 JSON 方式展現)。讀者能夠在這個工具頂部選擇「JavaScript」語言和「typescript」編譯器,查看 TypeScript 生成的語法樹結構。
爲了幫助英文文盲們更好地理解語法類型,讀者可參考:https://github.com/meriyah/meriyah/wiki/ESTree-Node-Types-Table
雖然語法節點種類不少,但其實只有四類:
在 TypeScript 中,節點命名比較規範,通常類型節點以 TypeNode 結尾;表達式節點以 Expression 結尾;語句節點以 Statement 結尾。
好比 if 語句節點:
export interface IfStatement extends Statement { kind: SyntaxKind.IfStatement; expression: Expression; thenStatement: Statement; elseStatement?: Statement; }
鑑於有些讀者對部分語法比較陌生,這裏能夠說明一些可能未正確理解的節點類型
export interface ExpressionStatement extends Statement, JSDocContainer {
kind: SyntaxKind.ExpressionStatement;
expression: Expression;
}
表達式是不能直接出如今最外層的,但如下代碼是容許的:
var x = 1;
1 + 1; // 這是表達式
由於 1 + 1 是表達式,它們同時又是一個表達式語句。因此以上代碼的語法樹如圖:
常見的賦值、函數調用語句都實際上是一個表達式語句。
一對「{}」自己也是一個語句,稱爲塊語句。一個塊語句能夠包含若干個語句。
export interface Block extends Statement {
kind: SyntaxKind.Block;
statements: NodeArray<Statement>; /*@internal*/ multiLine?: boolean; }
好比 while 語句的主體只能是一條語句:
export interface IterationStatement extends Statement { statement: Statement; } export interface WhileStatement extends IterationStatement { kind: SyntaxKind.WhileStatement; expression: Expression; }
但 while 裏面明明是能夠寫不少語句的:
while(x_d) {
var a = 120; var b = 100; }
本質上,當咱們使用 {} 時,就已經使用了一個塊語句,while 的主體仍然是一個語句:塊語句。其它語句都是塊語句的子節點。
export interface LabeledStatement extends Statement, JSDocContainer {
kind: SyntaxKind.LabeledStatement;
label: Identifier;
statement: Statement;
}
經過標籤語句能夠爲語句命名,好比:
label: var x = 120;
命名後有啥用?能夠在 break 或 continue 中引用該名稱,以此實現跨級 break 和 continue 的效果:
export interface BreakStatement extends Statement {
kind: SyntaxKind.BreakStatement;
label?: Identifier; // 跳轉的標籤名
} export interface ContinueStatement extends Statement { kind: SyntaxKind.ContinueStatement; label?: Identifier; // 跳轉的標籤名 }
好比 x + y * z 中,須要先算乘號。生成的語法樹節點以下:
經過節點的層次關係,實現了這種優先級的效果(由於永遠不會把圖裏的 x 和 y 先處理)。
所以建立語法樹的同時,也就處理了優先級的問題,括號徹底能夠從語法樹中刪除。
一個複雜的類,也能用語法樹表示?
固然,任何語法最後都是用語法樹表達的,只不過類確實複雜一些:
export interface Declaration extends Node {
_declarationBrand: any;
}
export interface NamedDeclaration extends Declaration {
name?: DeclarationName; } export interface ClassLikeDeclarationBase extends NamedDeclaration, JSDocContainer { kind: SyntaxKind.ClassDeclaration | SyntaxKind.ClassExpression; name?: Identifier; typeParameters?: NodeArray<TypeParameterDeclaration>; heritageClauses?: NodeArray<HeritageClause>; members: NodeArray<ClassElement>; } export interface ClassDeclaration extends ClassLikeDeclarationBase, DeclarationStatement { kind: SyntaxKind.ClassDeclaration; /** May be undefined in `export default class { ... }`. */ name?: Identifier; }
類、函數、變量、導入聲明嚴格意義上是獨立的一種語法分類,但鑑於它和其它語句用法一致,爲了便於理解,這裏把聲明做語句的一種看待。
當源代碼被解析成語法樹後,源代碼就再也不須要了。若是後續流程發現一個錯誤,編譯器須要向用戶報告,並指出錯誤位置。
爲了能夠獲得這個位置,須要將節點在源文件種的位置保存下來:
export interface TextRange {
pos: number;
end: number;
}
export interface Node extends TextRange { kind: SyntaxKind; flags: NodeFlags; parent: Node; // ...(略) }
經過節點的 parent 能夠找到節點的根節點,即所在的文件;經過節點的 pos 和 end 能夠肯定節點在源文件的行列號(具體已經在第二節:標記位置 中介紹)。
爲了方便程序中遍歷任意節點,TypeScript 提供了一個工具函數:
/**
* Invokes a callback for each child of the given node. The 'cbNode' callback is invoked for all child nodes
* stored in properties. If a 'cbNodes' callback is specified, it is invoked for embedded arrays; otherwise,
* embedded arrays are flattened and the 'cbNode' callback is invoked for each element. If a callback returns
* a truthy value, iteration stops and that value is returned. Otherwise, undefined is returned.
*
* @param node a given node to visit its children
* @param cbNode a callback to be invoked for all child nodes
* @param cbNodes a callback to be invoked for embedded array
*
* @remarks `forEachChild` must visit the children of a node in the order
* that they appear in the source code. The language service depends on this property to locate nodes by position.
*/
export function forEachChild<T>(node: Node, cbNode: (node: Node) => T | undefined, cbNodes?: (nodes: NodeArray<Node>) => T | undefined): T | undefined { if (!node || node.kind <= SyntaxKind.LastToken) { return; } switch (node.kind) { case SyntaxKind.QualifiedName: return visitNode(cbNode, (<QualifiedName>node).left) || visitNode(cbNode, (<QualifiedName>node).right); case SyntaxKind.TypeParameter: return visitNode(cbNode, (<TypeParameterDeclaration>node).name) || visitNode(cbNode, (<TypeParameterDeclaration>node).constraint) || visitNode(cbNode, (<TypeParameterDeclaration>node).default) || visitNode(cbNode, (<TypeParameterDeclaration>node).expression); case SyntaxKind.ShorthandPropertyAssignment: return visitNodes(cbNode, cbNodes, node.decorators) || visitNodes(cbNode, cbNodes, node.modifiers) || visitNode(cbNode, (<ShorthandPropertyAssignment>node).name) || visitNode(cbNode, (<ShorthandPropertyAssignment>node).questionToken) || // ...(略) } }
forEachChild 函數只會遍歷節點的直接子節點,若是用戶須要遞歸遍歷全部子節點,須要遞歸調用 forEachChild。forEachChild 接收一個函數用於遍歷,並容許用戶返回一個 true 型的值並終止循環。
掌握語法樹是掌握整個編譯系統的基礎。你應該能夠深入地知道語法樹的大概樣子,並清楚每種語法的語法樹結構。若是尚未完全掌握,可使用上文推薦的工具。
下一節將介紹生成語法樹的全過程。【不定時更新】