# Typescript 類別 ###### tags: `Typescript` ![](https://i.imgur.com/rJnjBQJ.png) ## 一、類別宣告 ### 1. 定義類別 若我們想要定義類別 C,其中類別包含屬性 P 與方法 M。其中,P 對應之型別為 Tp,而方法 M 對應之函式型別為 (paramName: Tparam): Toutput,則最基本的宣告方式為 ```Typescript= class c { P: Tp; M(paramName: Tparam): Toutput { // 方法內的內容 } } ``` ### 2. 透過類別建立物件 若已宣告完類別 C,可以藉由 new 關鍵字從 C 建立出物件 O ``` let O = new C(/* 可能也會包含參數*/); ``` ```Typescript= class CPersonInfo { name: string = 'Maxwell'; age: number = 20; hasPet: boolean = false; printInfo() { console.log(` Name: ${this.name} Age: ${this.age} Own a pet? ${this.hasPet} `) } } let maxwellINfoFromClass = new CpersonInfo(); ``` ### 3.類別建構子 Constructor Function - 在使用 new 建構新物件時,能夠傳入一些參數設定物件的內容,這個函式我們稱之為類別建構函式,又或者是類別建構子 ```Typescript= class CPersonInfo { name: string = 'Maxwell'; age: number = 20; hasPet: boolean = false; // 建構子函式 constructor(name: string, age: number, hasPet: boolean) { this.name = name; this.age = age; this.hasPet = hasPet; } printInfo() { console.log(` Name: ${this.name} Age: ${this.age} Own a pet? ${this.hasPet} `) } } let maxwellINfoFromClass = new CpersonInfo(); ``` - 透過類別建構子可以預設函式參數,也能夠傳入參數 ```typescript= let customInfo1 = new CustomPersonInfo(); customInfo1.printInfo(); /* Name: Maxwell Age: 20 Owns a pet? false */ let customInfo2 = new CustomPersonInfo('Toby', 32, true); costomInfo2.printInfo(); /* Name: Toby Age: 32 Owns a pet? true */ ``` - 類別建構子函式有以下特點: - 專門進行物件初始化的動作:若宣告某類別 C,則每一次建立屬於 C 類別的物件時,最先跑的程序即是建構子函式裡的內容。而 C 本身就是那個建構子函式。 - 類別的宣告不一定要存在建構子函式,而建構子函式的預設值是空函式。(即 function() {})前提是,該類別沒有繼承自其他的類別(Inheritance)。 - 若要在宣告類別時定義建構子函式的程式內容,必須使用關鍵字 constructor 作為建構子名稱。可直接把建構子當成函式撰寫;若建構子含有參數部分,必須積極註記。 - 建構子函式通常只用來進行初始化成員變數,強烈建議禁止塞其他的邏輯進去 - 若必須要在物件建構之初,執行其他的 Business Logic,則建議將這些程序進行抽象化(Abstraction)並定義為當前類別之下的方法後,在建構子函式進行呼叫的動作。(通常抽象化過後的方法,會被標記為私有 private) ## 二、 類別存取修飾子 TypeScript Class Access Modifiers ## 一、定義範例 type, interface ```typescript= type TUserAccount = { account: string, password: string, money: number } interface AccountSystem { users: TUserAccount[]; currentUser: TUserAccount | undefined; signIn(account: string, password: string): void; signOut():void; } interface TransactionSystem { deposit(amount: number): void; withdraw(amount: number): void; } interface ICashMachine extends TransactionSystem, AccountSystem {} ``` ### 2. implements 類別如果要根據某介面的規格實踐出來,可以使用 implement ```typescript= class CashMachine implements ICashMachine { users: TUserAccount[] = [ { account: 'Maxwell', password: '123', money: 10000 }, ]; currentUser: TUserAccount | undefined = undefined; signIn(account: string, password: string) { for (let i = 0; i < this.users.length; i += 1) { const user = this.users[i]; if ( user.account === account && user.password === password ) { this.currentUser = user; break; } } if (this.currentUser === undefined) { throw new Error('User not found') } } signOut() { this.currentUser = undefined } deposit(amount: number) { if (this.currentUser !== undefined) { this.currentUser.money -= amount; } else { throw new Error('No User signed in') } } withdraw(amount: number) { if (this.currentUser !== undefined) { this.currentUser.money -= amount } else { throw new Error('No user signed in') } } } ``` ### 3.存取修飾子的使用與意義 Access Modifiers 限制成員變數或方法被呼叫的權限。 - 存取修飾子總共分為三種模式:**public**、**private** 以及 **protected** - 存取修飾子可以調整成員變數(Member Variables)與方法(Member Methods)在類別裡面與類別外部的使用限制。 - 類別在宣告時,若成員變數或方法沒有被註記上存取修飾子,預設就是 public 模式。 - 若宣告某類別 C,則裡面的成員變數 P 或成員方法 M 被註記為: > - public 模式時:P 與 M 可以任意在類別內外以及繼承 C 的子類別使用 > - private 模式時:P 與 M 僅僅只能在當前類別 C 內部使用 > - protected 模式時: P 與 M 除了當前類別 C 內部使用外,繼承 C 的子類別也可以使用 - 若宣告某類別 C,其中該類別有明確實踐(implements)某介面 I,則類別 C 必須實踐所有介面 I 所提供的格式 —— 而介面 I 的規格轉換成為類別 C 時 —— 成員變數與方法皆必須為 public 模式 ### 4. 介面相對於類別的意義 TypeScript 介面(Interface)定義的是功能的完整規格外,若類別對介面進行綁定的動作,裡面的規格細目代表類別成員 public 模式的成員。 => **interface 如果有定義包含 private 屬性的東西事實上是不合理的!** ```typescript= interface AccountSystem { // users: TUserAccount[]; // currentUser: TUserAccount | undefined; signIn(account: string, password: string): void; signOut():void; } ``` - 因為 machine.currentUser 為私有方法,只能在 CashMachine 內部使用。 ![](https://i.imgur.com/GfvGbID.png) ### 5. 初始化成員變數 我們還可以將成員變數的值在建構子函式(Constructor Function)裡進行初始化動作。 ```typescript= class CashMachine implements ICashMachine { private users: TUserAccount[]; constructor (users: TUserAccount[]) { this.user = user } } ``` 更簡潔的寫法 除了將 users 宣告成成員變數外,也直接把 users 設定為 private 模式 —— 也因此我們不需要再建構子函式內寫這一行:this.users = users。`` ```typescript= class CashMachine implements ICashMachine { constructor (private users: TUserAccount[]) {} } ``` ## 三、類別繼承 ### 1. 使用類別繼承 1. 宣告類別 C ``` class C { public Ppublic: T1; private Pprivate: T2; protected Pprotected: T3; public Mpublic(/*參數*/) {} public Mpublic(/*參數*/) {} public Mpublic(/*參數*/) {} } ``` 2. 另外宣告類別 D, 使用 estend 讓 D 繼承 C ``` class D extend c {} ``` C 為 D 的父類別(Parent Class/Superclass) D 為 C 的子類別(Child Class/Subclass)。 - D 類別可以使用 C 類別非 private 模式的成員變數與方法們(D 除了 Pprivate 與 Mprivate 外,其他成員都可以使用) - D 類別建造出來的物件(使用 new),該物件的型別除了屬於 D 類別以外 —— 由於繼承的關係,該物件的型別也同時屬於類別 C - 相對地,C 類別所建造出來的物件型別為 C 類別,但不屬於 D 類別 ### 2.子類別不能使用父類別的 private 方法 因此把父類別的存取修飾子改為 protect >D 類別可以使用 C 類別非 private 模式的成員變數與方法們(D 除了 Pprivate 與 Mprivate 外,其他成員都可以使用) ### 3.子類別如何初始化~ Super - 使用 Super 連結父類別的建構子函式進行物件成員變數初始化的動作。 - 在子類別裡,super 可以等效於父類別的建構子函式。 1. 子類別的建構子注意事項 - 子類別的建構子函式裡,進行初始化物件時 —— 也就是**super 被呼叫之前**,由於物件還未建立完畢,不能有 this 相關的操作行為 - 假設宣告某類別 C,而另外一個類別 D 繼承 C。另外,C 類別的建構子函式裡擁有若干參數 ...args 並且子類別也沒有實作建構子函式時,則預設的子類別建構子函式的行為為: ![](https://i.imgur.com/bNyzYom.png) ![](https://i.imgur.com/un1H4rJ.png) ## 四、靜態屬性 ### 1.類別的靜態屬性與方法 - 不需要經由建構物件的過程,而是直接從類別本身提供的屬性與方法,皆稱之為靜態屬性與方法,又被稱為靜態成員(Static Members)。 - 因此,靜態成員具有一個很重要的特點:不管物件被建立多少次,靜態成員只會有一個版本 —— 這也符合靜態的概念:固定、單一版本、不變的原則等等。 - 使用靜態成員的狀況: - 靜態成員不會隨著物件建構的不同而隨之改變 - 靜態成員可以作為類別本身提供的工具,不需要經過建構物件的程序;換句話說:類別提供之靜態成員本身就是可被操作的介面 - 將原本動態的 class 改為靜態 原本的class屬性及方法必須物件建構後才能使用 ```typescript= // 動態成員版本的幾何圓形類別 class CircleGeometry { private PI: number = 3.14 constructor(public radius: number) {} public area(): number { return this.PI * (this.radius ** 2) } public circumference(): number { return 2 * this.PI * this.radius } } const myCircle = new CircleGeometry(2) console.log(myCircle.area()) console.log(myCircle.circumference()) ``` - 改為靜態成員後,靜態成員綁在類別上,可以直接從類別取用 - 由於 PI 從成員變成了靜態成員,因此不能使用 this.PI,而是 StaticCircleGeometry.PI 來取用。 ```typescript= // 靜態成員版本的幾何圖形類別 class StaticCircleGeometry { static PI: number = 3.14 static area (radius: number): number { return StaticCircleGeometry.PI * (radius ** 2) } static circumference(radius: number): number { return 2 * StaticCircleGeometry.PI * radius; } } const areaFromStaticMethod = StaticCircleGeometry.area(2) const circumferenceFromStaticMethod = StaticCircleGeometry.circumference(3) console.log(areaFromStaticMethod, circumferenceFromStaticMethod) ``` > 若想要在某類別 C 宣告的靜態屬性 Pstatic 與方法 Mstatic ``` class C { static Pstatic: Tstatic = Vstatic; static Mstatic(/*參數*/): Treturn_static {/* 方法內容 */} } ``` - static 與存取修飾子 Access Modifiers - private 模式下不可從外部讀取 - 因此可以在內部使用 getValueOfPI 這個方法來拿取 PI 值 ```typescript= // private 版本 class StaticCircleGeometry { private static PI: number = 3.14 static area (radius: number): number { return StaticCircleGeometry.PI * (radius ** 2) } static circumference(radius: number): number { return 2 * StaticCircleGeometry.PI * radius; } static getValueOfPI(): number { return StaticCircleGeometry.PI } } const areaFromStaticMethod = StaticCircleGeometry.area(2) const circumferenceFromStaticMethod = StaticCircleGeometry.circumference(3) console.log(StaticCircleGeometry.getValueOfPI()) console.log(areaFromStaticMethod, circumferenceFromStaticMethod) ``` ## 五、取值方法與存值方法 Access Getter Methods 或 Setter Methods ### 1. get - get 關鍵字搭配想要取的名稱 ```typescript= class CircleGeometryV2 { private PI: number = 3.14 constructor(public radius:number) {} get area() { return this.PI * (this.radius ** 2) } public circumference(): number { return 2 * this.PI * this.radius; } } const randomCircle = new CircleGeometryV2(2) console.log(randomCircle.area); ``` - 只要物件的狀態被改變,存取方法們計算過後的值也會被自動更新! ```typescript= var randomCircle = new CircleGeometryV2(2); console.log(randomCircle.area); // 12.56 randomCircle.radius = 3; console.log(randomCircle.area); // 28.26 ``` ### 2. set - 專門在模擬屬性被指派值的情況,因此會需要一個參數去代表被指派的值,所以存值方法的型別會多一個參數 (value: number): void。 - 對 area 指派值,radius 也自動被更改了! ```typescript= class CircleGeometryV2 { private PI: number = 3.14 constructor(public radius:number) {} get area() { return this.PI * (this.radius ** 2) } set area(value: number) { this.radius = (value / this.PI) ** 0.5 } public circumference(): number { return 2 * this.PI * this.radius; } } const randomCircle = new CircleGeometryV2(2) console.log(randomCircle.area); randomCircle.area = 3.14 * (5 ** 2) console.log(randomCircle.radius); // 5 ``` ### 3. 類別的存取方法 Accessors - 取值方法專門在模擬呼叫物件的屬性時的行為;存值方法則是在模擬指派值到物件屬性的行為:由於兩者皆是用方法的方式來呈現屬性的呼叫與指派行為,因此才會被稱為存取方法。(而不是存取屬性) - 若只有單純實踐某物件屬性的取值方法(Getter Method)而沒有相對應的存值方法,該屬性可以模擬唯讀(Read-only)的狀態。 - 取值方法的實踐不能有任何參數。若某屬性是利用取值方法來模擬的話,呼叫該物件的屬性,型別推論的結果會等同於取值方法回傳的值之型別。又因為是在模擬物件取值的過程,因此不回傳值的行為也是錯誤的! - 存值方法只能有一個參數,而該參數代表的值是指派的值。根據函式型別篇章提出的重點,我們必須對存值方法內部的參數進行積極註記的動作。若某屬性的指派行為是用存值方法模擬,則該屬性被指派錯誤的值也會根據存值方法的參數被註記到的型別進行比對。 - 若想要在類別 C 宣告某存取方法模擬物件呼叫或指派值到屬性 P,其中 P 必須被指派的值之型別為 Tassign,則程式碼的格式為:類別的存取方法 Accessors ```typescript= class C { get P() { return <some-value> } set P(value) { } } ``` ## 六、私有建構子 X 單身狗模式 - Private Constructor & Singleton Pattern SingletonC 實踐單例模式,則必須符合: - SingletonC 的建構子函式為 private 模式 - SingletonC 必須要有私有靜態屬性專門存放單例模式下的唯一物件(該物件又被稱為 Singleton,或單子),習慣上該靜態屬性的名稱為 Instance - SingletonC 必須要有公用靜態方法負責把單子回傳出來,是唯一一個取得單子的途徑,習慣上該靜態方法的名稱為 getInstance ```typescript= class SingletonC { private constructor(/* 成員變數或參數 */) { /* 初始化單子的過程 */ } private static Instance: SingletonC = new SingletonC(/* 參數 */) static getInstance(): SingletonC { return this.Instance; } } ``` ![](https://i.imgur.com/3ycMdg1.png) ### 2. 單例類別的繼承 Singleton Class Inheritance 另外,對單例模式的類別進行繼承的動作,並不違反單例模式的初衷。在單例模式下運用繼承的目的有二: - 子類別可以擴充該個體的功能(可能擴充靜態方法等) - 多個子類別的單例物件可以在程式中隨時抽換 ### 3. 懶漢模式 Lazy Initialization in Singleton Pattern 由於單例模式是在剛初始化類別的時候就順便將單子給建構好。有時候會遇到單子建構過程中會耗費龐大的資源; 懶漢模式: 第一次呼叫到 SingletonClass.getInstance 這個靜態方法時,到時候再建造就好了 ![](https://i.imgur.com/wOPPdNO.png) ## 七、類別推論 X 註記類別 ### 1. 普通類別之型別推論與註記行為 ```typescript= enum Color { White, Black, Brown, Grey, Rainbow } class Horse { constructor ( public name: string, public color: Color, public readonly type: string, private noise: string = 'meeee' ) {} public makeNoise() { console.log(this.noise) } public info() { console.log(this.infoText) } private infoText(): string { return `It is ${this.name} the ${Color[this.color]} ${this.type}` } } const horse1 = new Horse('pony',Color.White, 'small', 'yayayya') ``` - 建構子函式的參數被 Typescript 推論的結果 - 宣告類別(Class)就相當於宣告一個函式 - 回傳型別是 Horse,代表類別本身是一種型別化名(Type Alias);也就是說,宣告一個類別等於建造新的型別 ![](https://i.imgur.com/1zU9Gqb.png) - 若變數被指派的值為類別 C 建構出來的物件,則 TypeScript 會自動推論該變數之型別為 C。 被推論出型別為 C 的變數符合廣義物件完整性原則: - 該變數不能夠新增屬性 - 該變數在原有屬性下不能指派錯誤的型別的值 - 要完整覆寫該變數,指派的值必須是類別 C 建構出來的物件 ![](https://i.imgur.com/h3U9tjL.png) ### 2. 繼承過後的類別之型別推論與註記行為 - 被註記為父類別的變數可以指派子類別物件,因為他們屬於同一個原型鍊 ```typescript= enum Color { White, Black, Brown, Grey, Rainbow } class Horse { constructor ( public name: string, public color: Color, public readonly type: string, private noise: string = 'meeee' ) {} public makeNoise() { console.log(this.noise) } public info() { console.log(this.infoText) } protected infoText(): string { return `It is ${this.name} the ${Color[this.color]} ${this.type}` } } class Unicorn extends Horse { constructor(name: string) { super(name, Color.Rainbow, 'Mystical Unicorn', 'Nheeeee~') } protected infoText(): string { return `It's a mystical unicorn! Its name is ${this.name}` } public puke(): void { console.log('Puking rainbow vomit'); } } const unicorn: Horse = new Unicorn('Pipi'); ``` - 有註記和沒註記父類別的差異 - 因為 Horse 類別沒有 puke 這個方法,如果積極註記變數,TS 會提醒錯誤 => **子類別新增了父類別沒有的成員,該成員若被呼叫時 —— 會被 TypeScript 警告。** ![](https://i.imgur.com/mooAsQW.png) - 將變數標記為子類別並指派給父類別的建構子函式 - 因為子類別新增了puke的方法,TS提出了警告 => **手動對該物件新增屬性或方法就會破壞掉物件的完整性** ![](https://i.imgur.com/2NfptEG.png) ### 3. 類別型別等效理論 若宣告兩個類別 C1 與 C2 —— 其中 C1 與 C2 的成員皆為 public 模式,並且所有的成員名稱對應型別皆相同,TypeScript 判定 C1 型別等效於 C2 型別。 若為 private 模式因為可以被自訂任意的行為,因此被 TypeScript 判定型別格式不等 ![](https://i.imgur.com/gp1ZdWt.png) ![](https://i.imgur.com/hRQjFaw.png) - 只要結構一樣,不管是 Type 或是 Interface,只要格式一樣都會通過,就算參數明確指定了 TA ```typescript= type TA = { hello: string } type TB = { hello: string } interface IA { hello: string } interface IB { hello: string } function logTypeA(obj: TA) { console.log(obj) } logTypeA(<TA>{ hello: 'World'}) logTypeA(<TB>{ hello: 'World'}) logTypeA(<IA>{ hello: 'World'}) logTypeA(<IB>{ hello: 'World'}) ``` ## 八、類別與介面 X 終極的組合 implements 負責將類別綁定介面的規格 類別一但跟介面綁定了,就必須實現介面裡描述的內容,否則會被 TypeScript 認定為違約 ```typescript= class chracter inplements ICharacter ``` - 類別繼承與介面綁定最大的不同 - 類別一次只能繼承一個父類別 - 類別可以同時實踐多個介面 ```typescript= class C {} interface Ia {} interface Ib {} interface Ic {} // ... interface In {} // 宣告類別 D 繼承自 C, 綁定介面 Ia, Ib, Ic, ..., In class D extends C implements Ia, Ib, Ic,..., In ``` - D 類別的宣告因為繼承自 C,因此 D 擁有 C 的所有 public 與 protected 模式下的成員。另外,由於 D 類別也有對介面 I1、I2、...In 進行綁定,因此必須實踐所有 I1、I2、...In 融合過後的結果之規格 ```typescript= enum Role { Swordman = 'Swordman', Warlock = 'Warlock', HighwayMan = 'HighwayMan', BountyHunter = 'BountyHunter', Monster = 'Monster' } interface ICharacter { name: string, role: Role, attack(target: ICharacter): void } interface IStats { health: number, mana: number, strength: number, defense: number, } class Character implements ICharacter, IStats { constructor( public name: string, public role: Role ){} public health = 50; public mana = 100; public strength = 60; public defense = 50; public attack(target: ICharacter) { let verb: string; switch (this.role) { case Role.Swordman: verb = 'attacking'; break; case Role.Warlock: verb = 'cursing'; break; case Role.HighwayMan: verb = 'ambush'; break; case Role.BountyHunter: verb = 'threatening'; break; default: throw new Error(`${this.role} didn't exist`) } console.log(`${this.role} is ${verb} ${target.name}`) } } class Monster implements ICharacter { public role = Role.Monster; constructor(public name: string) {} public attack(target: ICharacter) { console.log( ` ${this.name} is attacking the ${target.role} - ${target.name} `) } } ``` - 類別繼承通常不容易將功能拆出來再利用,因此耦合程度較高;然而,因為介面的實踐是可以拆卸又裝到不同的類別上去,因此介面與類別的耦合程度較低以外,可再利用度較高。