傳統的 JavaScript 使用函數和基於原型的繼承來建構可重複使用的元件,但對於習慣物件導向方法的程式設計師來說,這可能有點奇怪,在物件導向方法中,類別繼承功能,而物件則從這些類別建構。從 ECMAScript 2015(也稱為 ECMAScript 6)開始,JavaScript 程式設計師可以使用這個物件導向的類別基礎方法來建構他們的應用程式。在 TypeScript 中,我們允許開發人員立即使用這些技術,並將它們編譯成可在所有主要瀏覽器和平台上運作的 JavaScript,而無需等待下一個版本的 JavaScript。
類別
我們來看一個簡單的基於類別的範例
tsTryclassGreeter {greeting : string;constructor(message : string) {this.greeting =message ;}greet () {return "Hello, " + this.greeting ;}}letgreeter = newGreeter ("world");
如果你之前使用過 C# 或 Java,這個語法應該看起來很熟悉。我們宣告一個新的類別 Greeter。這個類別有三個成員:一個稱為 greeting 的屬性、一個建構函式和一個方法 greet。
你會注意到在類別中,當我們參照類別的成員之一時,我們會加上 this.。這表示它是成員存取。
在最後一行,我們使用 new 建構 Greeter 類別的一個實例。這會呼叫我們之前定義的建構函式,建立一個具有 Greeter 形狀的新物件,並執行建構函式來初始化它。
繼承
在 TypeScript 中,我們可以使用常見的物件導向模式。在基於類別的程式設計中最基本的模式之一,就是能夠延伸現有的類別來使用繼承建立新的類別。
讓我們來看一個範例
tsTryclassAnimal {move (distanceInMeters : number = 0) {console .log (`Animal moved ${distanceInMeters }m.`);}}classDog extendsAnimal {bark () {console .log ("Woof! Woof!");}}constdog = newDog ();dog .bark ();dog .move (10);dog .bark ();
此範例顯示最基本的繼承特徵:類別繼承基底類別的屬性和方法。在此,Dog 是一個衍生類別,使用 extends 關鍵字從 基底 類別 Animal 衍生。衍生類別通常稱為子類別,而基底類別通常稱為超類別。
由於 Dog 延伸了 Animal 的功能,因此我們可以建立一個 Dog 執行個體,它既可以 bark() 又可以 move()。
現在讓我們來看一個更複雜的範例。
tsTryclassAnimal {name : string;constructor(theName : string) {this.name =theName ;}move (distanceInMeters : number = 0) {console .log (`${this.name } moved ${distanceInMeters }m.`);}}classSnake extendsAnimal {constructor(name : string) {super(name );}move (distanceInMeters = 5) {console .log ("Slithering...");super.move (distanceInMeters );}}classHorse extendsAnimal {constructor(name : string) {super(name );}move (distanceInMeters = 45) {console .log ("Galloping...");super.move (distanceInMeters );}}letsam = newSnake ("Sammy the Python");lettom :Animal = newHorse ("Tommy the Palomino");sam .move ();tom .move (34);
此範例涵蓋了我們先前未提及的一些其他特徵。我們再次看到 extends 關鍵字用於建立兩個新的 Animal 子類別:Horse 和 Snake。
與前一個範例不同的是,包含建構函式的每個衍生類別必須呼叫 super(),它將執行基底類別的建構函式。此外,在建構函式主體中存取 this 上的屬性之前,我們必須呼叫 super()。這是一個 TypeScript 將強制執行的重要規則。
範例還顯示如何使用針對子類別進行專業化的方法來覆寫基底類別中的方法。在此,Snake 和 Horse 都建立一個 move 方法,它會覆寫 Animal 中的 move,並賦予其特定於每個類別的功能。請注意,即使 tom 被宣告為 Animal,由於其值是 Horse,因此呼叫 tom.move(34) 將呼叫 Horse 中的覆寫方法
Slithering... Sammy the Python moved 5m. Galloping... Tommy the Palomino moved 34m.
公開、私有和受保護的修飾詞
預設為公開
在我們的範例中,我們可以自由存取在我們的程式中宣告的成員。如果你熟悉其他語言的類別,你可能已經注意到在以上的範例中我們不需要使用 public 這個字來達成此目的;例如,C# 要求每個成員都必須明確標示為 public 才會可見。在 TypeScript 中,每個成員預設都是 public。
你仍然可以明確地將一個成員標記為 public。我們可以用以下的方式撰寫前一節的 Animal 類別
tsTryclassAnimal {publicname : string;public constructor(theName : string) {this.name =theName ;}publicmove (distanceInMeters : number) {console .log (`${this.name } moved ${distanceInMeters }m.`);}}
ECMAScript 私有欄位
使用 TypeScript 3.8,TypeScript 支援私有欄位的 JavaScript 新語法
tsTryclassAnimal {#name: string;constructor(theName : string) {this.#name =theName ;}}newProperty '#name' is not accessible outside class 'Animal' because it has a private identifier.18013Property '#name' is not accessible outside class 'Animal' because it has a private identifier.Animal ("Cat").#name ;
此語法內建於 JavaScript 執行環境中,而且可以更良好地保證每個私有欄位的隔離。目前,這些私有欄位最好的文件說明是在 TypeScript 3.8 發行說明 中。
了解 TypeScript 的 private
TypeScript 也有自己的方式來宣告一個成員被標記為 private,它無法從其包含的類別外部存取。例如
tsTryclassAnimal {privatename : string;constructor(theName : string) {this.name =theName ;}}newProperty 'name' is private and only accessible within class 'Animal'.2341Property 'name' is private and only accessible within class 'Animal'.Animal ("Cat").; name
TypeScript 是一種結構化類型系統。當我們比較兩個不同的類型時,無論它們來自何處,如果所有成員的類型相容,則我們會說這些類型本身相容。
然而,在比較具有 private 和 protected 成員的類型時,我們會以不同的方式處理這些類型。對於兩個被視為相容的類型,如果其中一個具有 private 成員,則另一個必須具有源自相同宣告的 private 成員。protected 成員也適用相同規則。
讓我們看一個範例,以更清楚了解這在實際應用中的運作方式
tsTryclassAnimal {privatename : string;constructor(theName : string) {this.name =theName ;}}classRhino extendsAnimal {constructor() {super("Rhino");}}classEmployee {privatename : string;constructor(theName : string) {this.name =theName ;}}letanimal = newAnimal ("Goat");letrhino = newRhino ();letemployee = newEmployee ("Bob");animal =rhino ;Type 'Employee' is not assignable to type 'Animal'. Types have separate declarations of a private property 'name'.2322Type 'Employee' is not assignable to type 'Animal'. Types have separate declarations of a private property 'name'.= animal employee ;
在此範例中,我們有一個 Animal 和一個 Rhino,其中 Rhino 是 Animal 的子類別。我們還有一個新的類別 Employee,在形狀方面看起來與 Animal 相同。我們建立這些類別的一些實例,然後嘗試將它們相互指派,以了解會發生什麼事。由於 Animal 和 Rhino 共享 Animal 中 private name: string 的相同宣告中的 private 側面,因此它們相容。然而,Employee 並非如此。當我們嘗試從 Employee 指派到 Animal 時,我們會收到一個錯誤,指出這些類型不相容。即使 Employee 也有稱為 name 的 private 成員,但它並非我們在 Animal 中宣告的那個。
了解 protected
protected 修飾詞的作用很像 private 修飾詞,但宣告為 protected 的成員也可以在衍生類別中存取。例如,
tsTryclassPerson {protectedname : string;constructor(name : string) {this.name =name ;}}classEmployee extendsPerson {privatedepartment : string;constructor(name : string,department : string) {super(name );this.department =department ;}publicgetElevatorPitch () {return `Hello, my name is ${this.name } and I work in ${this.department }.`;}}lethoward = newEmployee ("Howard", "Sales");console .log (howard .getElevatorPitch ());Property 'name' is protected and only accessible within class 'Person' and its subclasses.2445Property 'name' is protected and only accessible within class 'Person' and its subclasses.console .log (howard .); name
請注意,雖然我們無法從 Person 外部使用 name,但我們仍可以在 Employee 的實例方法中使用它,因為 Employee 從 Person 衍生。
建構函式也可以標記為 protected。這表示類別無法在包含它的類別外部實例化,但可以延伸。例如,
tsTryclassPerson {protectedname : string;protected constructor(theName : string) {this.name =theName ;}}// Employee can extend PersonclassEmployee extendsPerson {privatedepartment : string;constructor(name : string,department : string) {super(name );this.department =department ;}publicgetElevatorPitch () {return `Hello, my name is ${this.name } and I work in ${this.department }.`;}}lethoward = newEmployee ("Howard", "Sales");letConstructor of class 'Person' is protected and only accessible within the class declaration.2674Constructor of class 'Person' is protected and only accessible within the class declaration.john = newPerson ("John");
唯讀修飾詞
您可以使用 readonly 關鍵字將屬性設為唯讀。唯讀屬性必須在宣告時或在建構函式中初始化。
tsTryclassOctopus {readonlyname : string;readonlynumberOfLegs : number = 8;constructor(theName : string) {this.name =theName ;}}letdad = newOctopus ("Man with the 8 strong legs");Cannot assign to 'name' because it is a read-only property.2540Cannot assign to 'name' because it is a read-only property.dad .= "Man with the 3-piece suit"; name
參數屬性
在我們最後一個範例中,我們必須在 Octopus 類別中宣告一個唯讀成員 name 和一個建構函數參數 theName。這是為了讓 theName 的值在 Octopus 建構函數執行後可以存取。參數屬性 讓您可以在一個地方建立和初始化成員。以下是使用參數屬性的前一個 Octopus 類別的進一步修正
tsTryclassOctopus {readonlynumberOfLegs : number = 8;constructor(readonlyname : string) {}}letdad = newOctopus ("Man with the 8 strong legs");dad .name ;
請注意,我們完全捨棄了 theName,只在建構函數上使用縮寫的 readonly name: string 參數來建立和初始化 name 成員。我們已將宣告和指定合併到一個位置。
參數屬性是透過在建構函數參數之前加上存取修飾詞或 readonly,或兩者來宣告。對參數屬性使用 private 會宣告和初始化一個私有成員;同樣地,public、protected 和 readonly 也是如此。
存取器
TypeScript 支援 getter/setter,作為攔截物件成員存取的一種方式。這讓您可以更細緻地控制每個物件上成員的存取方式。
我們將轉換一個簡單類別來使用 get 和 set。首先,我們從沒有 getter 和 setter 的範例開始。
tsTryclassEmployee {fullName : string;}letemployee = newEmployee ();employee .fullName = "Bob Smith";if (employee .fullName ) {console .log (employee .fullName );}
雖然允許人們隨機直接設定 fullName 非常方便,但我們可能也希望在設定 fullName 時強制執行一些約束。
在此版本中,我們新增一個 setter 來檢查 newName 的長度,以確保它與我們的後端資料庫欄位的最大長度相容。如果它不相容,我們會擲回一個錯誤,通知用戶端程式碼發生了問題。
為了保留現有功能,我們也新增一個簡單的 getter 來擷取未修改的 fullName。
tsTryconstfullNameMaxLength = 10;classEmployee {private_fullName : string = "";getfullName (): string {return this._fullName ;}setfullName (newName : string) {if (newName &&newName .length >fullNameMaxLength ) {throw newError ("fullName has a max length of " +fullNameMaxLength );}this._fullName =newName ;}}letemployee = newEmployee ();employee .fullName = "Bob Smith";if (employee .fullName ) {console .log (employee .fullName );}
為了證明我們的存取器現在正在檢查值的長度,我們可以嘗試指定一個長度超過 10 個字元的姓名,並驗證我們是否會收到錯誤。
關於存取器,有幾件事需要注意
首先,存取器需要您將編譯器設定為輸出 ECMAScript 5 或更高版本。降級到 ECMAScript 3 不受支援。其次,具有 get 但沒有 set 的存取器會自動推斷為 readonly。當從您的程式碼產生 .d.ts 檔案時,這很有用,因為您的屬性使用者可以看到他們無法變更它。
靜態屬性
到目前為止,我們只討論過類別的實例成員,這些成員會在物件實例化時出現在物件上。我們也可以建立類別的靜態成員,這些成員會顯示在類別本身,而不是出現在實例上。在此範例中,我們在 origin 上使用 static,因為它是所有格線的通用值。每個實例都會透過在類別名稱前加上字首來存取這個值。類似於在實例存取前加上 this. 字首,在這裡我們在靜態存取前加上 Grid. 字首。
tsTryclassGrid {staticorigin = {x : 0,y : 0 };calculateDistanceFromOrigin (point : {x : number;y : number }) {letxDist =point .x -Grid .origin .x ;letyDist =point .y -Grid .origin .y ;returnMath .sqrt (xDist *xDist +yDist *yDist ) / this.scale ;}constructor(publicscale : number) {}}letgrid1 = newGrid (1.0); // 1x scaleletgrid2 = newGrid (5.0); // 5x scaleconsole .log (grid1 .calculateDistanceFromOrigin ({x : 10,y : 10 }));console .log (grid2 .calculateDistanceFromOrigin ({x : 10,y : 10 }));
抽象類別
抽象類別是基礎類別,其他類別可以從中衍生。它們不能直接實例化。與介面不同,抽象類別可以包含其成員的實作詳細資料。abstract 關鍵字用於定義抽象類別以及抽象類別中的抽象方法。
tsTryabstract classAnimal {abstractmakeSound (): void;move (): void {console .log ("roaming the earth...");}}
抽象類別中標記為抽象的方法不包含實作,而且必須在衍生類別中實作。抽象方法與介面方法共用類似的語法。兩者都定義方法的簽章,但不包含方法主體。但是,抽象方法必須包含 abstract 關鍵字,而且可以選擇性地包含存取修飾詞。
tsTryabstract classDepartment {constructor(publicname : string) {}printName (): void {console .log ("Department name: " + this.name );}abstractprintMeeting (): void; // must be implemented in derived classes}classAccountingDepartment extendsDepartment {constructor() {super("Accounting and Auditing"); // constructors in derived classes must call super()}printMeeting (): void {console .log ("The Accounting Department meets each Monday at 10am.");}generateReports (): void {console .log ("Generating accounting reports...");}}letdepartment :Department ; // ok to create a reference to an abstract typeCannot create an instance of an abstract class.2511Cannot create an instance of an abstract class.department = newDepartment (); // error: cannot create an instance of an abstract classdepartment = newAccountingDepartment (); // ok to create and assign a non-abstract subclassdepartment .printName ();department .printMeeting ();Property 'generateReports' does not exist on type 'Department'.2339Property 'generateReports' does not exist on type 'Department'.department .(); // error: department is not of type AccountingDepartment, cannot access generateReports generateReports
進階技巧
建構函數
在 TypeScript 中宣告類別時,實際上會同時建立多個宣告。第一個是類別實例的型別。
tsTryclassGreeter {greeting : string;constructor(message : string) {this.greeting =message ;}greet () {return "Hello, " + this.greeting ;}}letgreeter :Greeter ;greeter = newGreeter ("world");console .log (greeter .greet ()); // "Hello, world"
在此,當我們說 let greeter: Greeter 時,我們使用 Greeter 作為類別 Greeter 的實例型別。這對其他物件導向語言的程式設計師來說幾乎是第二本能。
我們也建立另一個值,我們稱之為建構函數。這是當我們對類別實例使用 new 時呼叫的函數。讓我們看看上述範例建立的 JavaScript,了解實際情況
tsTryletGreeter = (function () {functionGreeter (message ) {this.greeting =message ;}Greeter .prototype .greet = function () {return "Hello, " + this.greeting ;};returnGreeter ;})();letgreeter ;greeter = newGreeter ("world");console .log (greeter .greet ()); // "Hello, world"
在此,let Greeter 將會指定給建構函數。當我們呼叫 new 並執行此函數時,我們會取得類別的實例。建構函數也包含類別的所有靜態成員。另一種思考每個類別的方式是,它有一個實例端和一個靜態端。
讓我們修改一下範例,以顯示此差異
tsTryclassGreeter {staticstandardGreeting = "Hello, there";greeting : string;greet () {if (this.greeting ) {return "Hello, " + this.greeting ;} else {returnGreeter .standardGreeting ;}}}letgreeter1 :Greeter ;greeter1 = newGreeter ();console .log (greeter1 .greet ()); // "Hello, there"letgreeterMaker : typeofGreeter =Greeter ;greeterMaker .standardGreeting = "Hey there!";letgreeter2 :Greeter = newgreeterMaker ();console .log (greeter2 .greet ()); // "Hey there!"letgreeter3 :Greeter ;greeter3 = newGreeter ();console .log (greeter3 .greet ()); // "Hey there!"
在此範例中,greeter1 的運作方式與先前類似。我們實例化 Greeter 類別,並使用此物件。我們之前看過這個。
接下來,我們直接使用類別。在此,我們建立一個稱為 greeterMaker 的新變數。此變數將會保留類別本身,或換句話說,是其建構函數。在此,我們使用 typeof Greeter,也就是「給我 Greeter 類別本身的型別」,而不是實例型別。或者更精確地說,「給我稱為 Greeter 的符號的型別」,也就是建構函數的型別。此型別將包含 Greeter 的所有靜態成員,以及建立 Greeter 類別實例的建構函數。我們透過在 greeterMaker 上使用 new 來顯示這一點,建立 Greeter 的新實例,並像之前一樣呼叫它們。另外值得一提的是,變更靜態屬性是不被鼓勵的,在此 greeter3 在 standardGreeting 上有 "Hey there!" 而不是 "Hello, there"。
將類別用作介面
如同我們在先前章節中所述,類別宣告會建立兩件事:代表類別實例的類型和建構函式。由於類別會建立類型,因此您可以在與使用介面的相同位置使用它們。
tsTryclassPoint {x : number;y : number;}interfacePoint3d extendsPoint {z : number;}letpoint3d :Point3d = {x : 1,y : 2,z : 3 };