本文介紹TypeScript集面向?qū)ο蟪橄蟾合⒎庋b、多態(tài)三要素為一體的編程利器,類類型吹由。
在JavaScript(ES5)中僅支持通過(guò)函數(shù)和原型鏈繼承模擬類的實(shí)現(xiàn)(用于抽象業(yè)務(wù)模型、組織數(shù)據(jù)結(jié)構(gòu)并創(chuàng)建可重用組件)朱嘴,自 ES6 引入 class 關(guān)鍵字后倾鲫,它才開始支持使用與Java類似的語(yǔ)法定義聲明類。
TypeScript 作為 JavaScript 的超集萍嬉,自然也支持 class 的全部特性乌昔,并且還可以對(duì)類的屬性、方法等進(jìn)行靜態(tài)類型檢測(cè)壤追。
類
在實(shí)際業(yè)務(wù)中玫荣,任何實(shí)體都可以被抽象為一個(gè)使用類表達(dá)的類似對(duì)象的數(shù)據(jù)結(jié)構(gòu),且這個(gè)數(shù)據(jù)結(jié)構(gòu)既包含屬性大诸,又包含方法捅厂,比如我們?cè)谙路匠橄罅艘粋€(gè)狗的類贯卦。
class Dog {
?name: string;
?constructor(name: string) {
? ?this.name = name;
? }
?bark() {
? ?console.log('Woof! Woof!');
? }
}
const dog = new Dog('Q');
dog.bark(); // => 'Woof! Woof!'
首先,我們定義了一個(gè) class Dog 焙贷,它擁有 string 類型的 name 屬性撵割、bark 方法和一個(gè)構(gòu)造器函數(shù)。然后辙芍,我們通過(guò) new 關(guān)鍵字創(chuàng)建了一個(gè) Dog 的實(shí)例啡彬,并把實(shí)例賦值給變量 dog。最后故硅,我們通過(guò)實(shí)例調(diào)用了類中定義的 bark 方法庶灿。
如果使用傳統(tǒng)的 JavaScript 代碼定義類,我們需要使用函數(shù)+原型鏈的形式進(jìn)行模擬吃衅,如下代碼所示:
function Dog(name: string) {
?this.name = name; // ts(2683) 'this' implicitly has type 'any' because it does not have a type annotation.
}
Dog.prototype.bark = function () {
?console.log('Woof! Woof!');
};
const dog = new Dog('Q'); // ts(7009) 'new' expression, whose target lacks a construct signature, implicitly has an 'any' type.
dog.bark(); // => 'Woof! Woof!'
在第 1~ 3 行往踢,我們定義了 Dog 類的構(gòu)造函數(shù),并在構(gòu)造函數(shù)內(nèi)部定義了 name 屬性徘层,再在第 4 行通過(guò) Dog 的原型鏈添加 bark 方法峻呕。
和通過(guò) class 方式定義類相比,這種方式明顯麻煩不少趣效,而且還缺少靜態(tài)類型檢測(cè)瘦癌。因此,類是 TypeScript 編程中十分有用且不得不掌握的工具跷敬。
下面我們看一下關(guān)于類最主要的特性——繼承讯私,也是面向?qū)ο缶幊倘笠刂弧?/p>
繼承
在 TypeScript 中,使用 extends 關(guān)鍵字就能很方便地定義類繼承的抽象模式西傀,如下代碼所示:
class Animal {
?type = 'Animal';
?say(name: string) {
? ?console.log(`I'm ${name}!`);
? }
}
class Dog extends Animal {
?bark() {
? ?console.log('Woof! Woof!');
? }
}
const dog = new Dog();
dog.bark(); // => 'Woof! Woof!'
dog.say('Q'); // => I'm Q!
dog.type; // => Animal
上面的例子展示了類最基本的繼承用法斤寇。比如第 8 ~12 行定義的Dog是派生類,它派生自第 1~6 行定義的Animal基類池凄,此時(shí)Dog實(shí)例繼承了基類Animal的屬性和方法抡驼。因此,在第 15~17 行我們可以看到肿仑,實(shí)例 dog 支持 bark致盟、say、type 等屬性和方法尤慰。
說(shuō)明:派生類通常被稱作子類馏锡,基類也被稱作超類(或者父類)。
細(xì)心的你可能發(fā)現(xiàn)了伟端,這里的 Dog 基類與第一個(gè)例子中的類相比杯道,少了一個(gè)構(gòu)造函數(shù)。這是因?yàn)?b>派生類如果包含一個(gè)構(gòu)造函數(shù)责蝠,則必須在構(gòu)造函數(shù)中調(diào)用 super() 方法党巾,這是 TypeScript 強(qiáng)制執(zhí)行的一條重要規(guī)則萎庭。
如下示例,因?yàn)榈?1~10 行定義的 Dog 類構(gòu)造函數(shù)中沒(méi)有調(diào)用 super 方法齿拂,所以提示了一個(gè) ts(2377) 的錯(cuò)誤驳规;而第 12~22 行定義的 Dog 類構(gòu)造函數(shù)中添加了 super 方法調(diào)用,所以可以通過(guò)類型檢測(cè)署海。
class Dog extends Animal {
?name: string;
?constructor(name: string) { // ts(2377) Constructors for derived classes must contain a 'super' call.
? ?this.name = name;
? }
?bark() {
? ?console.log('Woof! Woof!');
? }
}
class Dog extends Animal {
?name: string;
?constructor(name: string) {
? ?super(); // 添加 super 方法
? ?this.name = name;
? }
?bark() {
? ?console.log('Woof! Woof!');
? }
}
有些同學(xué)可能會(huì)好奇吗购,這里的 super() 是什么作用?其實(shí)這里的 super 函數(shù)會(huì)調(diào)用基類的構(gòu)造函數(shù)砸狞,如下代碼所示:
class Animal {
?weight: number;
?type = 'Animal';
?constructor(weight: number) {
? ?this.weight = weight;
? }
?say(name: string) {
? ?console.log(`I'm ${name}!`);
? }
}
class Dog extends Animal {
?name: string;
?constructor(name: string) {
? ?super(); // ts(2554) Expected 1 arguments, but got 0.
? ?this.name = name;
? }
?bark() {
? ?console.log('Woof! Woof!');
? }
}
將鼠標(biāo)放到第 15 行 Dog 類構(gòu)造函數(shù)調(diào)用的 super 函數(shù)上捻勉,我們可以看到一個(gè)提示,它的類型是基類 Animal 的構(gòu)造函數(shù):constructor Animal(weight: number): Animal 刀森。并且因?yàn)?Animal 類的構(gòu)造函數(shù)要求必須傳入一個(gè)數(shù)字類型的 weight 參數(shù)踱启,而第 15 行實(shí)際入?yún)榭眨蕴崾玖艘粋€(gè) ts(2554) 的錯(cuò)誤撒强;如果我們顯式地給 super 函數(shù)傳入一個(gè) number 類型的值禽捆,比如說(shuō) super(20)笙什,則不會(huì)再提示錯(cuò)誤了飘哨。
公共、私有與受保護(hù)的修飾符
類屬性和方法除了可以通過(guò) extends 被繼承之外琐凭,還可以通過(guò)修飾符控制可訪問(wèn)性芽隆。
在 TypeScript 中就支持 3 種訪問(wèn)修飾符,分別是 public统屈、private胚吁、protected。
·public 修飾的是在任何地方可見愁憔、公有的屬性或方法腕扶;
·private 修飾的是僅在同一類中可見、私有的屬性或方法吨掌;
·protected 修飾的是僅在類自身及子類中可見半抱、受保護(hù)的屬性或方法。
在之前的代碼中膜宋,示例類并沒(méi)有用到可見性修飾符窿侈,在缺省情況下,類的屬性或方法默認(rèn)都是 public秋茫。如果想讓有些屬性對(duì)外不可見史简,那么我們可以使用private進(jìn)行設(shè)置,如下所示:
class Son {
?public firstName: string;
?private lastName: string = 'Stark';
?constructor(firstName: string) {
? ?this.firstName = firstName;
? ? this.lastName; // ok
? }
}
const son = new Son('Tony');
console.log(son.firstName); //? => "Tony"
son.firstName = 'Jack';
console.log(son.firstName); //? => "Jack"
console.log(son.lastName); // ts(2341) Property 'lastName' is private and only accessible within class 'Son'.
在上面的例子中我們可以看到肛著,第 3 行 Son 類的 lastName 屬性是私有的圆兵,只在 Son 類中可見跺讯;第 2 行定義的 firstName 屬性是公有的,在任何地方都可見殉农。因此抬吟,我們既可以通過(guò)第 10 行創(chuàng)建的 Son 類的實(shí)例 son 獲取或設(shè)置公共的 firstName 的屬性(如第 11 行所示),還可以操作更改 firstName 的值(如第 12 行所示)统抬。
不過(guò)火本,對(duì)于 private 修飾的私有屬性,只可以在類的內(nèi)部可見聪建。比如第 6 行钙畔,私有屬性 lastName 僅在 Son 類中可見,如果其他地方獲取了 lastName 金麸,TypeScript 就會(huì)提示一個(gè) ts(2341) 的錯(cuò)誤(如第 14 行)擎析。
注意:TypeScript 中定義類的私有屬性僅僅代表靜態(tài)類型檢測(cè)層面的私有。如果我們強(qiáng)制忽略 TypeScript 類型的檢查錯(cuò)誤挥下,轉(zhuǎn)譯且運(yùn)行 JavaScript 時(shí)依舊可以獲取到 lastName 屬性揍魂,這是因?yàn)?JavaScript 并不支持真正意義上的私有屬性。
目前棚瘟,JavaScript 類支持 private 修飾符的提案已經(jīng)到 stage 3 了现斋。相信在不久的將來(lái),私有屬性在類型檢測(cè)和運(yùn)行階段都可以被限制為僅在類的內(nèi)部可見偎蘸。如果你感興趣的話庄蹋,可以在proposal-private-methods中進(jìn)行查看。
接下來(lái)我們?cè)倏匆幌率鼙Wo(hù)的屬性和方法迷雪,如下代碼所示:
class Son {
?public firstName: string;
?protected lastName: string = 'Stark';
?constructor(firstName: string) {
? ?this.firstName = firstName;
? ? this.lastName; // ok
? }
}
class GrandSon extends Son {
?constructor(firstName: string) {
? ?super(firstName);
? }
?public getMyLastName() {
? ?return this.lastName;
? }
}
const grandSon = new GrandSon('Tony');
console.log(grandSon.getMyLastName()); // => "Stark"
grandSon.lastName; // ts(2445) Property 'lastName' is protected and only accessible within class 'Son' and its subclasses.
在第 3 行限书,修改 Son 類的 lastName 屬性可見修飾符為 protected,表明此屬性在 Son 類及其子類中可見章咧。如示例第 6 行和第 16 行所示倦西,我們既可以在父類 Son 的構(gòu)造器中獲取 lastName 屬性值,又可以在繼承自 Son 的子類 GrandSon 的 getMyLastName 方法獲取 lastName 屬性的值赁严。
需要注意:雖然我們不能通過(guò)派生類的實(shí)例訪問(wèn)protected修飾的屬性和方法扰柠,但是可以通過(guò)派生類的實(shí)例方法進(jìn)行訪問(wèn)。比如示例中的第 21 行误澳,通過(guò)實(shí)例的 getMyLastName 方法獲取受保護(hù)的屬性 lastName 是 ok 的耻矮,而第 22 行通過(guò)實(shí)例直接獲取受保護(hù)的屬性 lastName 則提示了一個(gè) ts(2445) 的錯(cuò)誤。
只讀修飾符
在前面的例子中忆谓,Son 類 public 修飾的屬性既公開可見裆装,又可以更改值,如果我們不希望類的屬性被更改,則可以使用 readonly 只讀修飾符聲明類的屬性哨免,如下代碼所示:
class Son {
? public readonly firstName: string;
? constructor(firstName: string) {
? ? this.firstName = firstName;
? }
}
const son = new Son('Tony');
son.firstName = 'Jack'; // ts(2540) Cannot assign to 'firstName' because it is a read-only property.
在第 2 行茎活,我們給公開可見屬性 firstName 指定了只讀修飾符,這個(gè)時(shí)候如果再更改 firstName 屬性的值琢唾,TypeScript 就會(huì)提示一個(gè) ts(2540) 的錯(cuò)誤(參見第 9 行)载荔。這是因?yàn)橹蛔x屬性修飾符保證了該屬性只能被讀取,而不能被修改采桃。
注意:如果只讀修飾符和可見性修飾符同時(shí)出現(xiàn)懒熙,我們需要將只讀修飾符寫在可見修飾符后面。
存取器
除了上邊提到的修飾符之外普办,在 TypeScript 中還可以通過(guò)getter工扎、setter截取對(duì)類成員的讀寫訪問(wèn)。
通過(guò)對(duì)類屬性訪問(wèn)的截取衔蹲,我們可以實(shí)現(xiàn)一些特定的訪問(wèn)控制邏輯肢娘。下面我們把之前的示例改造一下,如下代碼所示:
class Son {
? public firstName: string;
? protected lastName: string = 'Stark';
? constructor(firstName: string) {
? ? this.firstName = firstName;
? }
}
class GrandSon extends Son {
? constructor(firstName: string) {
? ? super(firstName);
? }
? get myLastName() {
? ? return this.lastName;
? }
? set myLastName(name: string) {
? ? if (this.firstName === 'Tony') {
? ? ? this.lastName = name;
? ? } else {
? ? ? console.error('Unable to change myLastName');
? ? }
? }
}
const grandSon = new GrandSon('Tony');
console.log(grandSon.myLastName); // => "Stark"
grandSon.myLastName = 'Rogers';
console.log(grandSon.myLastName); // => "Rogers"
const grandSon1 = new GrandSon('Tony1');
grandSon1.myLastName = 'Rogers'; // => "Unable to change myLastName"
在第 14~24 行舆驶,我們使用 myLastName 的getter橱健、setter重寫了之前的 GrandSon 類的方法,在 getter 中實(shí)際返回的是 lastName 屬性沙廉。然后拘荡,在 setter 中,我們限定僅當(dāng) lastName 屬性值為 'Tony' 蓝仲,才把入?yún)?name 賦值給它俱病,否則打印錯(cuò)誤官疲。
在第 28 行中袱结,我們可以像訪問(wèn)類屬性一樣訪問(wèn)getter,同時(shí)也可以像更改屬性值一樣給setter賦值途凫,并執(zhí)行一些自定義邏輯垢夹。
在第 27 行,因?yàn)?grandSon 實(shí)例的 lastName 屬性被初始化成了 'Tony'维费,所以在第 29 行我們可以把 'Rogers' 賦值給 setter 果元。而 grandSon1 實(shí)例的 lastName 屬性在第 32 行被初始化為 'Tony1',所以在第 33 行把 'Rogers' 賦值給 setter 時(shí)犀盟,打印了我們自定義的錯(cuò)誤信息而晒。
靜態(tài)屬性
以上介紹的關(guān)于類的所有屬性和方法,只有類在實(shí)例化時(shí)才會(huì)被初始化阅畴。實(shí)際上倡怎,我們也可以給類定義靜態(tài)屬性和方法。
因?yàn)檫@些屬性存在于類這個(gè)特殊的對(duì)象上,而不是類的實(shí)例上监署,所以我們可以直接通過(guò)類訪問(wèn)靜態(tài)屬性颤专,如下代碼所示:
class MyArray {
? static displayName = 'MyArray';
? static isArray(obj: unknown) {
? ? return Object.prototype.toString.call(obj).slice(8, -1) === 'Array';
? }
}
console.log(MyArray.displayName); // => "MyArray"
console.log(MyArray.isArray([])); // => true
console.log(MyArray.isArray({})); // => false
在第 2~3 行,通過(guò) static 修飾符钠乏,我們給 MyArray 類分別定義了一個(gè)靜態(tài)屬性 displayName 和靜態(tài)方法 isArray栖秕。之后,我們無(wú)須實(shí)例化 MyArray 就可以直接訪問(wèn)類上的靜態(tài)屬性和方法了晓避,比如第 8 行訪問(wèn)的是靜態(tài)屬性 displayName簇捍,第 9~10 行訪問(wèn)的是靜態(tài)方法 isArray。
基于靜態(tài)屬性的特性俏拱,我們往往會(huì)把與類相關(guān)的常量垦写、不依賴實(shí)例 this 上下文的屬性和方法定義為靜態(tài)屬性,從而避免數(shù)據(jù)冗余彰触,進(jìn)而提升運(yùn)行性能梯投。
靜態(tài)屬性特點(diǎn):
? ?1. 一聲明就被存儲(chǔ)在棧中,直接占據(jù)內(nèi)存况毅,可以快速穩(wěn)定的調(diào)用分蓖。
? ?2. 生命周期長(zhǎng),從jvm加載開始到j(luò)vm加載結(jié)束尔许。
? ?3. 全局唯一么鹤,在一個(gè)運(yùn)行環(huán)境中,靜態(tài)變量只有一個(gè)值味廊,任何一次修改都是全局性的影響蒸甜。
? ?4. 占據(jù)內(nèi)存,程序中應(yīng)該包含盡量少的static
注意:上邊我們提到了不依賴實(shí)例 this 上下文的方法就可以定義成靜態(tài)方法余佛,這就意味著需要顯式注解 this 類型才可以在靜態(tài)方法中使用 this柠新;非靜態(tài)方法則不需要顯式注解 this 類型,因?yàn)?this 的指向默認(rèn)是類的實(shí)例辉巡。
抽象類
接下來(lái)我們看看關(guān)于類的另外一個(gè)特性——抽象類恨憎,它是一種不能被實(shí)例化僅能被子類繼承的特殊類。
我們可以使用抽象類定義派生類需要實(shí)現(xiàn)的屬性和方法郊楣,同時(shí)也可以定義其他被繼承的默認(rèn)屬性和方法憔恳,如下代碼所示:
abstract class Adder {
? abstract x: number;
? abstract y: number;
? abstract add(): number;
? displayName = 'Adder';
? addTwice(): number {
? ? return (this.x + this.y) * 2;
? }
}
class NumAdder extends Adder {
? x: number;
? y: number;
? constructor(x: number, y: number) {
? ? super();
? ? this.x = x;
? ? this.y = y;
? }
? add(): number {
? ? return this.x + this.y;
? }
}
const numAdder = new NumAdder(1, 2);
console.log(numAdder.displayName); // => "Adder"
console.log(numAdder.add()); // => 3
console.log(numAdder.addTwice()); // => 6
在第 1~10 行,通過(guò) abstract 關(guān)鍵字净蚤,我們定義了一個(gè)抽象類 Adder钥组,并通過(guò)abstract關(guān)鍵字定義了抽象屬性x、y及方法add今瀑,而且任何繼承 Adder 的派生類都需要實(shí)現(xiàn)這些抽象屬性和方法程梦。
同時(shí)腔丧,我們還在抽象類 Adder 中定義了可以被派生類繼承的非抽象屬性displayName和方法addTwice。
然后作烟,我們?cè)诘?12~23 行定義了繼承抽象類的派生類 NumAdder愉粤, 并實(shí)現(xiàn)了抽象類里定義的 x、y 抽象屬性和 add 抽象方法拿撩。如果派生類中缺少對(duì) x衣厘、y、add 這三者中任意一個(gè)抽象成員的實(shí)現(xiàn)压恒,那么第 12 行就會(huì)提示一個(gè) ts(2515) 錯(cuò)誤影暴,關(guān)于這點(diǎn)你可以親自驗(yàn)證一下。
抽象類中的其他非抽象成員則可以直接通過(guò)實(shí)例獲取探赫,比如第 26~28 行中型宙,通過(guò)實(shí)例 numAdder,我們獲取了 displayName 屬性和 addTwice 方法伦吠。
因?yàn)槌橄箢惒荒鼙粚?shí)例化妆兑,并且派生類必須實(shí)現(xiàn)繼承自抽象類上的抽象屬性和方法定義,所以抽象類的作用其實(shí)就是對(duì)基礎(chǔ)邏輯的封裝和抽象毛仪。
實(shí)際上搁嗓,我們也可以定義一個(gè)描述對(duì)象結(jié)構(gòu)的接口類型抽象類的結(jié)構(gòu),并通過(guò) implements 關(guān)鍵字約束類的實(shí)現(xiàn)箱靴。
使用接口與使用抽象類相比腺逛,區(qū)別在于接口只能定義類成員的類型,如下代碼所示:
interface IAdder {
? x: number;
? y: number;
? add: () => number;
}
class NumAdder implements IAdder {
? x: number;
? y: number;
? constructor(x: number, y: number) {
? ? this.x = x;
? ? this.y = y;
? }
? add() {
? ? return this.x + this.y;
? }
? addTwice() {
? ? return (this.x + this.y) * 2;
? }
}
在第 1~5 行衡怀,我們定義了一個(gè)包含 x棍矛、y、add 屬性和方法的接口類型抛杨,然后在第 6~12 行實(shí)現(xiàn)了擁有接口約定的x够委、y 屬性和 add 方法,以及接口未約定的 addTwice 方法的NumAdder類 蝶桶。
類的類型
類的最后一個(gè)特性——類的類型和函數(shù)類似慨绳,即在聲明類的時(shí)候,其實(shí)也同時(shí)聲明了一個(gè)特殊的類型(確切地講是一個(gè)接口類型)真竖,這個(gè)類型的名字就是類名,表示類實(shí)例的類型厌小;在定義類的時(shí)候恢共,我們聲明的除構(gòu)造函數(shù)外所有屬性、方法的類型就是這個(gè)特殊類型的成員璧亚。如下代碼所示:
class A {
? name: string;
? constructor(name: string) {
? ? this.name = name;
? }
}
const a1: A = {}; // ts(2741) Property 'name' is missing in type '{}' but required in type 'A'.
const a2: A = { name: 'a2' }; // ok
在第 1~6 行讨韭,我們?cè)诙x類 A ,也說(shuō)明我們同時(shí)定義了一個(gè)包含字符串屬性 name 的同名接口類型 A。因此透硝,在第 7 行把一個(gè)空對(duì)象賦值給類型是 A 的變量 a1 時(shí)狰闪,TypeScript 會(huì)提示一個(gè) ts(2741) 錯(cuò)誤,因?yàn)槿鄙?name 屬性濒生。在第 8 行把對(duì)象{ name: 'a2' }賦值給類型同樣是 A 的變量 a2 時(shí)埋泵,TypeScript 就直接通過(guò)了類型檢查,因?yàn)橛?name 屬性罪治。