在上一節主要介紹了語法樹的解析生成。就好比電腦已經聽到了“你真聰明”這句話,現在要讓電腦開始思考這句話的含義——是真聰明還是假聰明。
這是一個非常的復雜的過程,接下來將有連續幾節內容介紹實現原理,本節則主要提前介紹一些相關的概念。
符號
在代碼里面,可以定義一個變量、一個函數、或者一個類,這些定義都有一個名字,然后在其它地方可以通過名字引用這個定義。
這些定義統稱為符號(Symbol)(注意和 ES6 里的 Symbol 不相干)。
當在代碼里寫了一個標識符名稱(變量名),這個名稱一定可以解析為某個符號(可能是變量、參數、函數或其它的,可能是用戶自己寫的,也可能系統自帶),如果無法解析,那一定是一個錯誤。
一個符號一般和一個聲明節點對應,比如一個變量符號就對應一個 var 聲明節點或 let/const 聲明節點。
可能存在一個符號對應多個聲明節點的情況,比如:
class A{}
interface A{}
同名的類和接口只產生一個符號 A,但這個符號擁有兩個聲明。
可能存在一個符號沒有源聲明節點的情況,比如:
type T = {[key: string]: any}
T 有無限個成員,每個成員都是沒有源聲明節點的。
TS 中符號的定義:
export interface Symbol { flags: SymbolFlags; // Symbol flags escapedName: __String; // Name of symbol declarations: Declaration[]; // Declarations associated with this symbol valueDeclaration: Declaration; // First value declaration of the symbol members?: SymbolTable; // Class, interface or object literal instance members exports?: SymbolTable; // Module exports globalExports?: SymbolTable; // Conditional global UMD exports /* @internal */ id?: number; // Unique id (used to look up SymbolLinks) /* @internal */ mergeId?: number; // Merge id (used to look up merged symbol) /* @internal */ parent?: Symbol; // Parent symbol /* @internal */ exportSymbol?: Symbol; // Exported symbol associated with this symbol /* @internal */ nameType?: Type; // Type associated with a late-bound symbol /* @internal */ constEnumOnlyModule?: boolean; // True if module contains only const enums or other modules with only const enums /* @internal */ isReferenced?: SymbolFlags; // True if the symbol is referenced elsewhere. Keeps track of the meaning of a reference in case a symbol is both a type parameter and parameter. /* @internal */ isReplaceableByMethod?: boolean; // Can this Javascript class property be replaced by a method symbol? /* @internal */ isAssigned?: boolean; // True if the symbol is a parameter with assignments /* @internal */ assignmentDeclarationMembers?: Map<Declaration>; // detected late-bound assignment declarations associated with the symbol }
其中,declarations 表示關聯的源節點。valueDeclaration 則表示第一個具有值的源節點。
注意兩者都可能為 `undefined`,源碼中之所以沒將它們標上 `?`,主要是因為作者懶(不然代碼需要經常判斷空)。
其中,escapedName 表示符號的名稱,名稱本質是字符串,TS 在源碼中有一些內部的特殊符號名稱,這些名稱都以“__”前綴,如果用戶本身就定義了名字帶__的,會被轉義成其它名字,所以 TS 內部將轉義后的名字標記成 __String 類型,運行期間它本質還是字符串,所以不影響性能。
作用域
允許定義符號的節點叫作用域(Scope),比如全局范圍(源文件),函數,大括號。
在同一個作用域中,不能定義同名的符號。
作用域是一個樹結構,查找變量時,先在就近的作用域查找,找不到就向外層查找。
如圖共四個作用域:
仔細觀察會發現,if 語句不是一個作用域,但 for 語句卻是。
語言本身就是這么設計的,因為在 for 里面可以聲明變量。
流程節點
流程節點是執行流程圖的組成部分。
比如 a = b > c && d == 3 ? e : f 的執行順序:
由於 JS 是動態語言,變量的類型可能隨執行的流程發生變化,因此在分析時需要知道整個代碼的執行順序。
流程節點就是擁有記錄這個順序的對象。
開始流程
開始流程是整個流程圖的根節點。
// FlowStart represents the start of a control flow. For a function expression or arrow // function, the node property references the function (which in turn has a flowNode // property for the containing control flow). export interface FlowStart extends FlowNodeBase { node?: FunctionExpression | ArrowFunction | MethodDeclaration; }
代碼總是從一個函數開始執行的,所以開始流程也會記錄關聯的函數聲明。
標簽流程
如果執行的時候出現判斷,則可能從一個流程進入兩個子流程,即流程跳轉,跳轉的目標叫標簽流程:
// FlowLabel represents a junction with multiple possible preceding control flows. export interface FlowLabel extends FlowNodeBase { antecedents: FlowNode[] | undefined; }
縮小類型范圍的流程
理論上,每行節點都可能對變量的類型有影響,比如上圖例子中,e 所在的位置在流程上可以確認 d == 3。
那么 d == 3 就是一種縮小類型范圍的流程,在這個流程節點后面,統一認為 d 就是 3。
TS 目前並不支持將所有的表達式都按縮小類型范圍的流程處理,只支持特定的幾種表達式,甚至有些表達式如果加了括號就不認識。
這主要是基於性能考慮,這樣可以更少創建流程節點。
TS 目前支持的流程有:
// FlowAssignment represents a node that assigns a value to a narrowable reference, // i.e. an identifier or a dotted name that starts with an identifier or 'this'. export interface FlowAssignment extends FlowNodeBase { node: Expression | VariableDeclaration | BindingElement; antecedent: FlowNode; } export interface FlowCall extends FlowNodeBase { node: CallExpression; antecedent: FlowNode; } // FlowCondition represents a condition that is known to be true or false at the // node's location in the control flow. export interface FlowCondition extends FlowNodeBase { node: Expression; antecedent: FlowNode; } export interface FlowSwitchClause extends FlowNodeBase { switchStatement: SwitchStatement; clauseStart: number; // Start index of case/default clause range clauseEnd: number; // End index of case/default clause range antecedent: FlowNode; } // FlowArrayMutation represents a node potentially mutates an array, i.e. an // operation of the form 'x.push(value)', 'x.unshift(value)' or 'x[n] = value'. export interface FlowArrayMutation extends FlowNodeBase { node: CallExpression | BinaryExpression; antecedent: FlowNode; }
finally 流程
try finally 流程稍微有些麻煩。
try { // 1 // ...(其它代碼)... // 2 } catch { // 3 // ...(其它代碼)... // 4 } finally { // 5 } // 6
對於位置 5,其父流程是 1/2/3/4 (假設 try 中任何一行代碼都可能報錯)
對於位置 6,其父流程只能是 5,且此時的 5 的父流程只能是 2/4,
所以位置 6 的父流程節點是 finally 結束位置的節點 5,而節點 5 的父流程有兩種可能:1/2/3/4 或只有 2/4
所以 TS 在 5 的前后插入了兩個特殊的流程節點:StartFinally 和 EndFinally,
當遍歷到 EndFinally 時,則給 StartFinally 加鎖,說明此時需要的是 finally 之后的流程節點,否則說明需要的是 finally 本身的父節點
export interface AfterFinallyFlow extends FlowNodeBase, FlowLock {
antecedent: FlowNode;
}
export interface PreFinallyFlow extends FlowNodeBase {
antecedent: FlowNode;
lock: FlowLock;
}
綜合
以上所有類型的流程節點,通過父節點的方式組成一個圖
export type FlowNode = | AfterFinallyFlow | PreFinallyFlow | FlowStart | FlowLabel | FlowAssignment | FlowCall | FlowCondition | FlowSwitchClause | FlowArrayMutation;
如果將流程節點可視化,將類似地鐵圖,每個節點就是一個站點,站點之間的線路存在分叉,也存在合並,也存在循環。
自測題
為了幫助驗證到此為止的知識點是否都已掌握,這里准備了一些題目:
1. 解釋以下術語:
- Token
- Node
- SyntaxKind
- Symbol
- Declaration
- TypeNode
- FlowNode
- Increamentable
- fullStart
- Trivial
2. 闡述編譯器從源文件到符號的流程步驟
3. 猜測編譯器在生成符號后的操作內容