TypeScript類、介面、繼承
TS引入了 Class(類)這個概念,作為物件的模板。通過class
關鍵字,可以定義類。
基本上,TS的 class
可以看作只是一個語法糖,它的絕大部分功能,ES5 都可以做到,新的 class
寫法只是讓物件原型的寫法更加清晰、更像面向物件程式設計的語法而已。
類
定義一個類
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '(' + this.x + ', ' + this.y + ')' ;
}
}
使用這個類
let p=new Point(1,2);
需要注意的地方有以下幾點:
①類和模組內部預設採用嚴格模式,不需要使用 use strict
指定執行模式。
② constructor
方法是類的預設方法,通過new
命令生成物件例項時,自動呼叫該方法。一個類必須有 constructor
方法,如果沒有顯式定義,一個空的 constructor
方法會被預設新增,這一點與Java的類一致。
③必須使用 new
命令來呼叫 class
,否則將會報錯。
④類不存在變數提升,只有先宣告類,才能使用類。
⑤類的方法內部如果含有 this
,它預設指向類的例項。但是如果我們單獨將其方法提取出來, this
值可能會指向當前執行的環境。為了防止這種事情的發生,我們可以使用箭頭函式(箭頭函式的 this
值指向初始化的函式)。
public、private、protected和readonly
public
、private
、protected
和readonly
都是類的成員(屬性)修飾符
public
在TS裡,成員都預設為public
。被public
修飾的屬性,我們在類的內外都可以自由訪問到這些被定義的屬性。
class Animal {
public name: string;
public constructor(theName: string) { this.name = theName; }
}
new Animal("Cat").name;//Cat
private
當成員被標記成private時,它就不能在宣告它的類的外部訪問。
class Animal {
private name: string;
constructor(theName: string) { this.name = theName; }
}
new Animal("Cat").name;//Error!: Property 'name' is private and only accessible within class 'Animal'.
TS使用的是結構性型別系統。 當我們比較兩種不同的型別時,並不在乎它們從何處而來,如果所有成員的型別都是相容的,我們就認為它們的型別是相容的。
這裡的比較並非我們說得 ==
或者 ===
的比較,而是對期望值(結構)的比較。
class Animal1 {
name: string;
constructor(theName: string) { this.name = theName; }
}
class Animal2 {
name: string;
constructor(theName: string) { this.name = theName; }
}
//這樣的寫法是不會出錯的
let a:Animal1=new Animal2("cat");
但是被 private
或 protected
修飾的成員型別不一樣。如果其中一個型別裡包含一個private
(或protected
)成員,那麼當另外一個型別中也存在這樣一個private
(或protected
)成員, 並且它們都是來自同一處宣告時,那麼這兩個型別是相容的,否則是不相容的。
class Animal1 {
private name: string;
constructor(theName: string) { this.name = theName; }
}
class Animal2 extends Animal1{
constructor(theName: string) {super(name); }
}
class Animal3 {
private name: string;
constructor(theName: string) { this.name = theName; }
}
let animal1: Animal1 = new Animal2("cat");//沒問題。Animal1和Animal2的private修飾的成員變數name都來自於Animal1(都是來自同一處宣告)。
let animal3: Animal1 = new Animal3("cat");//ERROR:Type 'Animal3' is not assignable to type 'Animal3'.
protected
protected
修飾符與 private
修飾符的行為很相似,但有一點不同,protected
成員在派生類中仍然可以訪問。
使用 private
修飾的父類成員,派生類無法訪問。
class Person {
private name: string;
constructor(name: string) { this.name = name; }
}
class Employee extends Person {
constructor(name: string) { super(name)}
public sayName() {
return this.name;//ERROR!: Property 'name' is private and only accessible within class 'Person'.
}
}
let xiaoming = new Employee("xiaoming");
console.log(xiaoming.sayName());
使用protected
修飾的父類成員,在派生類中仍然可以訪問
class Person {
protected name: string;
constructor(name: string) { this.name = name; }
}
class Employee extends Person {
constructor(name: string) { super(name)}
public sayName() {
return this.name;
}
}
//派生類中仍能繼續使用
let xiaoming = new Employee("xiaoming");
console.log(xiaoming.sayName());
readonly修飾符
readonly
關鍵字與 public
、 private
和 protected
不一樣,它修飾的不是成員的訪問許可權,而是成員的再賦值許可權。
使用readonly
關鍵字將屬性設定為只讀的。 只讀屬性必須在宣告時或建構函式裡被初始化。
class Octopus {
readonly name: string;
readonly numberOfLegs: number = 8;
constructor (theName: string) {
this.name = theName;
}
}
let dad = new Octopus("Man with the 8 strong legs");
dad.name = "Man with the 3-piece suit"; // 錯誤! name 是隻讀的.
抽象類
抽象類做為其它派生類的基類使用。 它們一般不會直接被例項化。 不同於介面,抽象類可以包含成員的實現細節。
abstract
關鍵字是用於定義抽象類和在抽象類內部定義抽象方法。
abstract class Animal {
abstract makeSound(): void;// 必須在派生類中實現
move(): void {
console.log('roaming the earch...');
}
}
注意:
①抽象類中的抽象方法不包含具體實現並且必須在派生類中實現。
②抽象方法必須包含abstract
關鍵字並且可以包含訪問修飾符。
介面
在傳統的面向物件概念中,一個類可以擴充套件另一個類,也可以實現一個或多個介面。一個介面可以實現一個或多個介面但是不能擴充套件另一個類或介面。wiki百科中對 OOP 中介面的定義是:
在面向物件的語言中,術語
interface
經常被用來定義一個不包含資料和邏輯程式碼但是用函式簽名定義了行為的抽象型別。
但是對於TS來說,介面更重要的意義是對值所具有的 結構 進行型別檢查。
介面根據屬性劃分,可以劃分成三類,一種是必選屬性,另一種是可選屬性,還有一種就是只讀屬性。
必選屬性
必選屬性就是函式必須要有的屬性。
interface PersonVaule{
name:string;
age:number;
}
function Person(person:PersonVaule){
this.name=person.name;
this.age=person.age;
}
//建立例項
var xiaoming=new Person({name:"xiaoming",age:18})
型別檢查器並不會檢查屬性的順序,但是必須要必選屬性。
var xiaoming2=new Person({age:18,name:"xiaoming"})//沒有問題
var xiaoming3=new Person({name:"xiaoming"})//提示屬性缺失:Property 'age' is missing in type '{ name: string; }'.
可選屬性
接口裡的屬性不全都是必需的。 有些是隻在某些條件下存在,或者根本不存在。 可選屬性在應用“option bags
”模式時很常用,即給函式傳入的引數物件中只有部分屬性賦值了。
帶有可選屬性的介面與普通的介面定義差不多,只是在可選屬性名字定義的後面加一個?
符號。
interface AnimalVaule{
name?:string;
eat:string;
lifestyle?:string;
}
function Animal(animal:AnimalVaule){
this.name=animal.name;
this.eat=animal.eat;
this.lifestyle=animal.lifestyle;
}
let cat=new Animal({eat:"食肉動物",lifestyle:"晝伏夜出"});
可選屬性好處有二:
1. 可以對可能存在的屬性進行預定義
2. 可以捕獲引用了不存在的屬性時的錯誤。
下面這個例子就出現了錯誤提示:
let dog=new Animal({eat:"適應性的肉食類動物",lifestle:"晝行夜伏"})//'lifestle' does not exist in type 'AnimalVaule'.
只讀屬性
一些物件屬性只能在物件剛剛建立的時候修改其值。 你可以在屬性名前用readonly
來指定只讀屬性:
interface Point {
readonly x: number;
readonly y: number;
}
你可以通過賦值一個物件字面量來構造一個Point
。 賦值後,x
和y
再也不能被改變了。
let p1: Point = { x: 10, y: 20 };
p1.x = 5; // error!
readonly
和 const
readonly
和 const
宣告的變數或屬性都不允許二次修改。這兩個屬性的使用區別在於是作為變數還是屬性:
做為變數使用的話用const
,
做為屬性則使用readonly
。
介面不僅僅能描述物件的屬性,還能描述函式型別,可索引型別和類型別。
函式型別
為了使用介面表示函式型別,我們需要給介面定義一個呼叫簽名。 它就像是一個只有引數列表和返回值型別的函式定義。引數列表裡的每個引數都需要名字和型別。
interface SearchFunc {
(source: string, subString: string): boolean;
}
let mySearch:SearchFunc=function(src,sub){
let result = src.search(sub);
return result > -1;
}
注意:
函式的引數會逐個進行檢查,要求對應介面的位置上的引數型別是相容的,無需名稱一致。
可索引型別
與使用介面描述函式型別差不多,我們也可以描述那些能夠“通過索引得到”的型別,比如a[10
]或ageMap["daniel"]
。可索引型別具有一個 索引簽名 ,它描述了物件索引的型別,還有相應的索引返回值型別。
索引簽名共有兩種形式:字串和數字。
數字索引簽名:
interface NN {[index: number]: number;}
let nn: NN = [1, 2];
interface NS {[index: number]: string;}
let ns: NS = ["1", "2"];
上面例子裡,我們定義了 NN
介面和 NS
介面,它們具有索引簽名。 這個索引簽名表示了當用 number
去索引 NN
或NS
介面 時會得到 number
型別或 string
的返回值。
字串索引簽名:
字串索引簽名能夠很好的描述 dictionary
模式,並且它們也會確保所有屬性與其返回值型別相匹配。
interface SS {[index:string]:string}
let ss: SS = {"A":"a", "B":"b"};
interface SN {[index: string]: number;}
let sn: SN = {"A":1, "B":2};
你可以將索引簽名設定為只讀,這樣就防止了給索引賦值:
interface ReadonlyStringArray {
readonly [index: number]: string;
}
let myArray: ReadonlyStringArray = ["Alice", "Bob"];
myArray[2] = "Mallory"; // error!
索引的返回值可以不只一個,但是必須是同一個型別。
interface NN {
[index: number]: number;
length:number;
name: string // 錯誤,`name`的型別與索引型別返回值的型別不匹配
}
let nn: NN = [1, 2];
注意: 如果有多個返回值,那麼數字索引的返回值必須是字串索引返回值型別的子型別。
對於上述的解釋,TS原話是這樣的:
這是因為當使用
number
來索引時,JavaScript會將它轉換成string
然後再去索引物件。 也就是說用100(一個number
)去索引等同於使用”100”(一個string
)去索引,因此兩者需要保持一致。
雖然字面上的解釋不明所以,但是我們通過例子可以去理解其含義。
class Animal {
name: string;
}
class Dog extends Animal {
breed: string;
}
//ERROR!: Numeric index type 'Animal' is not assignable to string index type 'Dog'.
interface NotOkay {
[x: number]: Animal;
[x: string]: Dog;
}
對於上述例子, number
索引的返回值是父類Animal
,而 string
索引的返回是子類 Dog
。所以TS報錯。
如果修改成 number
索引的返回值是子類Dog
,string
索引的返回值是父類 Animal
,則毫無問題。
class Animal {
name: string;
}
class Dog extends Animal {
breed: string;
}
interface Okay {
[x: number]: Dog;
[x: string]: Animal;
}
類型別
與C#或Java裡介面的基本作用一樣,TS也能夠用它來明確的強制一個類去符合某種契約。
interface ClockInterface {
currentTime: Date;
}
class Clock implements ClockInterface {
currentTime: Date;
constructor(h: number, m: number) { }
}
注意:介面描述了類的公共部分,而不是公共和私有兩部分。 它不會幫你檢查類是否具有某些私有成員。
繼承
TS允許我們通過extends
關鍵字來 建立子類(實現繼承)。
下面這個例子,Dog
類繼承自 Animal
類,在Dog
類中我們可以訪問父類 Animal
的屬性和方法。
class Animal {
name: string;
constructor(theName: string) { this.name = theName; }
}
class Dog extends Animal {
breed: string;
}
new Dog("mydog").name;//mydog
注意:包含建構函式的派生類必須呼叫super()
,它會執行基類的構造方法。