TypeScript中有一些獨特的概念,來自需要描述JavaScript對象類型發生了哪些變化。舉個例子,最為獨特的概念就是"聲明合並"。理解了這個概念將會對你在當前JavaScript項目中使用TypeScript開發很有幫助。同時也打開了了解更高級抽象概念的門。
就本文目的而言,聲明合並是指編譯器執行將兩個名稱相同的聲明合並到一個單獨的聲明里的工作。合並后的聲明具有兩種原始聲明的特性。當然,聲明合並不限於合並兩個聲明,需要合並的聲明數量可任意(注意:他們之間具有相同名稱)。
基本概念
在TypeScript中,一個聲明可以有三種情況:命名空間/模塊(命名空間:內部模塊;模塊:外部模塊)、類型、值。當聲明是創建一個命名空間/模塊的時候,該命名空間可通過點符號(.)來訪問。創建類型的聲明將會創建一個指定名稱和結構的類型。最后,聲明值就是那些在輸出的JavaScript中可見的部分(如,函數和變量)。
Declaration Type | Namespace | Type | Value |
---|---|---|---|
Namespace | X | X | |
Class | X | X | |
Enum | X | X | |
Interface | X | ||
Type Alias | X | ||
Function | X | ||
Variable | X |
理解每一個聲明創建的是什么,將有助於你理解當執行聲明合並時對什么進行合並。
接口合並
最簡單也是最常見的聲明合並就是接口合並。在編譯最底層,聲明合並機制將兩個已聲明的成員加入到一個相同名稱的接口中。
interface Box { height: number; width: number; } interface Box { scale: number; } var box: Box = {height: 5, width: 6, scale: 10};
接口中的非函數成員必須是唯一的,如果兩個/多個接口同時聲明相同名稱的非函數成員,編譯器就會扔出一個錯誤。
對於函數成員,相同名稱的函數成員被視為這個函數的重載。值的注意的是,接口A和后面的接口A(這里成為A')合並,接口A‘中重載的函數將會比接口A中的同一個函數具有更高的優先級。
看案例:
interface Document { createElement(tagName: any): Element; } interface Document { createElement(tagName: string): HTMLElement; } interface Document { createElement(tagName: "div"): HTMLDivElement; createElement(tagName: "span"): HTMLSpanElement; createElement(tagName: "canvas"): HTMLCanvasElement; }
這三個接口將被合並到一個單獨的聲明。注意,每組接口內部的順序依舊保持相同,只是每個組之間被合並,並且排在后面的接口的成員在新聲明中被放到前面。
interface Document { createElement(tagName: "div"): HTMLDivElement; createElement(tagName: "span"): HTMLSpanElement; createElement(tagName: "canvas"): HTMLCanvasElement; createElement(tagName: string): HTMLElement; createElement(tagName: any): Element; }
模塊合並
類似於接口,相同名稱的模塊也會對其成員進行合並。由於模塊會創建一個命名空間和一個值,我們需要理解它們是如何合並的。
合並命名空間的時候,每個模塊聲明的輸出接口的類型定義將進行合並,同名命名空間合並成一個單獨的內部包含合並后接口的命名空間。
合並值的時候,如果已存在一個給定名稱模塊,那么后面的模塊內的輸出成員將被添加到這個模塊。
看看這個例子中的Animal模塊的聲明合並:
module Animals {
export class Zebra { }
}
module Animals {
export interface Legged { numberOfLegs: number; }
export class Dog { }
}
相當於:
module Animals {
export interface Legged { numberOfLegs: number; }
export class Zebra { }
export class Dog { }
}
這個案例是學習模塊合並很好的開始,但是想要更完整的理解,我們還需要理解非導出成員發生了什么。非導出成員只在原始(未合並)模塊可見。這意味着,在合並之后,來自其他聲明的合並后成員不能訪問到非導出成員。
我們可以看個詳細的解釋:
module Animal { var haveMuscles = true; export function animalsHaveMuscles() { return haveMuscles; } } module Animal { export function doAnimalsHaveMuscles() { return haveMuscles; // <-- 錯誤, haveMuscles在這里不能訪問 } }
因為haveMuscles這個成員未被輸出,只有與其共享未合並的模塊的animalsHaveMuscles函數能夠訪問這個symbol。盡管doAnimalsHaveMuscles函數是合並后的模塊的一部分,但它還是不能訪問到這個其他被合並的同名模塊內的未輸出成員。
模塊與類、函數、枚舉的合並
模塊具有足夠的靈活性,它可以與其他類型的聲明進行合並。模塊的聲明必須遵循與其合並的聲明。最終合並后的聲明將包含兩個聲明類型的屬性。Typescript使用這個功能去實現一些JavaScript里的模式。
第一個模塊合並案例,我們將模塊和類合並。這給用戶提供了描述內部類的方法:
class Album { label: Album.AlbumLabel; } module Album { export class AlbumLabel{ name:string; show(){ console.log(this.name); } constructor(name:string){ this.name = name; } } } var newAlbum = new Album.AlbumLabel("Ys"); newAlbum.show();
合並后成員的可訪問性規則和上一節的"模塊合並"一樣,所以我們必須export AlbumLabel類,為了讓合並后的類可訪問它。最終的結果是一個類的內部存在另一個類。你也可以使用模塊現有的類添加更多的靜態成員。
class Test{ fn:Test.TestFn } module Test { export var Value:string = "World"; export class TestFn{ show(name:string){ console.log(name+" "+Value); } } } var newTest = new Test.TestFn(); newTest.show("Hello");
除了內部類的模式,你可能對JavaScript中創建一個函數稍后再擴展其屬性的做法已經很熟悉了。TypeScript使用聲明合並達到這個目的,並且確保了類型安全。
function buildLabel(name: string): string { return buildLabel.prefix + name + buildLabel.suffix; } module buildLabel { export var suffix = ""; export var prefix = "Hello, "; } alert(buildLabel("Sam Smith"));
同樣,模塊也可以用來擴展枚舉的靜態成員:
enum Color { red = 1, green = 2, blue = 4 } module Color { export function mixColor(colorName: string) { if (colorName == "yellow") { return Color.red + Color.green; } else if (colorName == "white") { return Color.red + Color.green + Color.blue; } else if (colorName == "magenta") { return Color.red + Color.blue; } else if (colorName == "cyan") { return Color.green + Color.blue; } } } alert(Color.mixColor("yellow"));
不被允許的合並
在TypeScript中,並非所有的合並都被允許。目前為止,類不能與類合並,變量和類不能合並,接口和類也不能合並。需要模仿類的合並,請參考上一節:Typescript Mixins(混合)