xmlstreamexception 引數實體未進行宣告_TypeScript之宣告合併
技術標籤:xmlstreamexception 引數實體未進行宣告
茫茫人海中與你相遇
相信未來的你不會很差
來源:https://www.tslang.cn/docs/handbook/declaration-merging.html
介紹
TypeScript中有些獨特的概念可以在型別層面上描述JavaScript物件的模型。 這其中尤其獨特的一個例子是“宣告合併”的概念。 理解了這個概念,將有助於操作現有的JavaScript程式碼。 同時,也會有助於理解更多高階抽象的概念。
對本檔案來講,“宣告合併”是指編譯器將針對同一個名字的兩個獨立宣告合併為單一宣告。 合併後的宣告同時擁有原先兩個宣告的特性。 任何數量的宣告都可被合併;不侷限於兩個宣告。
基礎概念
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;}let box: Box = {height: 5, width: 6, scale: 10};
介面的非函式的成員應該是唯一的。如果它們不是唯一的,那麼它們必須是相同的型別。如果兩個介面中同時聲明瞭同名的非函式成員且它們的型別不同,則編譯器會報錯。
對於函式成員,每個同名函式宣告都會被當成這個函式的一個過載。同時需要注意,當介面 A與後來的介面A合併時,後面的介面具有更高的優先順序。
如下例所示:
interface Cloner { clone(animal: Animal): Animal;}interface Cloner { clone(animal: Sheep): Sheep;}interface Cloner { clone(animal: Dog): Dog; clone(animal: Cat): Cat;}
這三個介面合併成一個宣告:
interface Cloner { clone(animal: Dog): Dog; clone(animal: Cat): Cat; clone(animal: Sheep): Sheep; clone(animal: Animal): Animal;}
注意每組接口裡的宣告順序保持不變,但各組介面之間的順序是後來的介面重載出現在靠前位置。
這個規則有一個例外是當出現特殊的函式簽名時。如果簽名裡有一個引數的型別是單一的字串字面量(比如,不是字串字面量的聯合型別),那麼它將會被提升到過載列表的最頂端。
比如,下面的介面會合併到一起:
interface Document { createElement(tagName: any): Element;}interface Document { createElement(tagName: "div"): HTMLDivElement; createElement(tagName: "span"): HTMLSpanElement;}interface Document { createElement(tagName: string): HTMLElement; createElement(tagName: "canvas"): HTMLCanvasElement;}
合併後的Document
將會像下面這樣:
interface Document { createElement(tagName: "canvas"): HTMLCanvasElement; createElement(tagName: "div"): HTMLDivElement; createElement(tagName: "span"): HTMLSpanElement; createElement(tagName: string): HTMLElement; createElement(tagName: any): Element;}
合併名稱空間
與介面相似,同名的名稱空間也會合並其成員。名稱空間會創建出名稱空間和值,我們需要知道這兩者都是怎麼合併的。
對於名稱空間的合併,模組匯出的同名介面進行合併,構成單一名稱空間內含合併後的介面。
對於名稱空間裡值的合併,如果當前已經存在給定名字的名稱空間,那麼後來的名稱空間的匯出成員會被加到已經存在的那個模組裡。
Animals
宣告合併示例:
namespace Animals { export class Zebra { }}namespace Animals { export interface Legged { numberOfLegs: number; } export class Dog { }}
等同於:
namespace Animals { export interface Legged { numberOfLegs: number; } export class Zebra { } export class Dog { }}
除了這些合併外,你還需要了解非匯出成員是如何處理的。非匯出成員僅在其原有的(合併前的)名稱空間內可見。這就是說合並之後,從其它名稱空間合併進來的成員無法訪問非匯出成員。
下例提供了更清晰的說明
namespace Animal { let haveMuscles = true; export function animalsHaveMuscles() { return haveMuscles; }}namespace Animal { export function doAnimalsHaveMuscles() { return haveMuscles; // Error, because haveMuscles is not accessible here }}
因為 haveMuscles
並沒有匯出,只有animalsHaveMuscles
函式共享了原始未合併的名稱空間可以訪問這個變數。doAnimalsHaveMuscles
函式雖是合併名稱空間的一部分,但是訪問不了未匯出的成員。
名稱空間與類和函式和列舉型別合併
名稱空間可以與其它型別的宣告進行合併。只要名稱空間的定義符合將要合併型別的定義。合併結果包含兩者的宣告型別。TypeScript使用這個功能去實現一些JavaScript裡的設計模式。
合併名稱空間和類
這讓我們可以表示內部類。
class Album { label: Album.AlbumLabel;}namespace Album { export class AlbumLabel { }}
合併規則與上面合併名稱空間小節裡講的規則一致,我們必須匯出AlbumLabel
類,好讓合併的類能訪問。合併結果是一個類並帶有一個內部類。你也可以使用名稱空間為類增加一些靜態屬性。
除了內部類的模式,你在JavaScript裡,建立一個函式稍後擴充套件它增加一些屬性也是很常見的。TypeScript使用宣告合併來達到這個目的並保證型別安全。
function buildLabel(name: string): string { return buildLabel.prefix + name + buildLabel.suffix;}namespace buildLabel { export let suffix = ""; export let prefix = "Hello, ";}console.log(buildLabel("Sam Smith"));
相似的,名稱空間可以用來擴充套件列舉型:
enum Color { red = 1, green = 2, blue = 4}namespace 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; } }}
非法的合併
TypeScript並非允許所有的合併。目前,類不能與其它類或變數合併。想要了解如何模仿類的合併,請參考TypeScript的混入。
模組擴充套件
雖然JavaScript不支援合併,但你可以為匯入的物件打補丁以更新它們。讓我們考察一下這個玩具性的示例:
// observable.jsexport class Observable<T> { // ... implementation left as an exercise for the reader ...}// map.jsimport { Observable } from "./observable";Observable.prototype.map = function (f) { // ... another exercise for the reader}
它也可以很好地工作在TypeScript中, 但編譯器對 Observable.prototype.map
一無所知。你可以使用擴充套件模組來將它告訴編譯器:
// observable.ts stays the same// map.tsimport { Observable } from "./observable";declare module "./observable" { interface Observable { map(f: (x: T) => U): Observable<U>; }}Observable.prototype.map = function (f) { // ... another exercise for the reader}// consumer.tsimport { Observable } from "./observable";import "./map";let o: Observable<number>;o.map(x => x.toFixed());
模組名的解析和用import/export
解析模組識別符號的方式是一致的。更多資訊請參考Modules。當這些宣告在擴充套件中合併時,就好像在原始位置被聲明瞭一樣。但是,你不能在擴充套件中宣告新的頂級宣告-僅可以擴充套件模組中已經存在的宣告。
全域性擴充套件
你也以在模組內部新增宣告到全域性作用域中。
// observable.tsexport class Observable<T> { // ... still no implementation ...}declare global { interface Array<T> { toObservable(): Observable; }}Array.prototype.toObservable = function () { // ...}
全域性擴充套件與模組擴充套件的行為和限制是相同的。
我們在虛擬的空間與你相遇,期待可以碰撞出不一樣的火花
公眾號ID:前端大聯盟掃碼關注最新動態