
介紹
傳統(tǒng)的 JavaScript 程序使用函數(shù)和基于原型的繼承來創(chuàng)建可重用的組件,但對于熟悉使用面向?qū)ο蠓绞降某绦騿T來講就有些棘手,因為他們用的是基于類的繼承并且對象是由類構(gòu)建出來的。 從 ECMAScript 2015,也就是 ECMAScript 6 開始,JavaScript 程序員將能夠使用基于類的面向?qū)ο蟮姆绞健?使用 TypeScript,我們允許開發(fā)者現(xiàn)在就使用這些特性,并且編譯后的 JavaScript 可以在所有主流瀏覽器和平臺上運(yùn)行,而不需要等到下個 JavaScript 版本。
類
下面來看一個使用 類 的例子:
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return 'Hello,' + this.greeting;
}
}
let greeter = new Greeter('World');
console.log(greeter); // {greeting: 'World'}
greeter.greet(); // Hello,World
如果你使用過 C# 或 Java,你會對這種語法非常熟悉。我們聲明一個 Greeter 類,這個類有三個成員:
- 一個
greeting的屬性 - 一個構(gòu)造函數(shù)
- 一個
greet方法
你會注意到,我們在引用任何一個類的成員的時候,都用了 this ,它表示我們訪問的是類的成員。
最后一行,我們用 new 構(gòu)造了 Greeter 類的一個實例。它會調(diào)用之前定義的構(gòu)造函數(shù),創(chuàng)建一個 Greeter 類型的新對象,并執(zhí)行構(gòu)造函數(shù)初始化它。
繼承
在 TypeScript 里,我們可以使用常用的面向?qū)ο竽J健;陬惖某绦蛟O(shè)計中一種最基本的模式是:允許使用繼承類拓展現(xiàn)有的類。
看下面 ?? 的例子 ??:
class Animal {
move(distanceInMeters: number = 0) {
console.log(`Animal moved ${distanceInMeters}m.`);
}
}
class Dog extends Animal {
bark() {
console.log('Woof! Woof!');
}
}
const dog = new Dog();
dog.bark(); // Woof! Woof!
dog.move(10); // Animal moved 10m.
dog.bark(); // Woof! Woof!
這個例子展示了最基本的繼承:類從基類中繼承了屬性和方法。這里, Dog 是一個派生類,它派生自 Animal 基類,通過 extends 關(guān)鍵字。派生類通常被稱作子類,基類通常被稱作 超類。
-
基類:在面向?qū)ο笤O(shè)計中,被定義為包含所有實體共性的
class類型。已存在的用來派生新類的類稱為基類,又稱父類。 - 派生類:從已定義類產(chǎn)生新類的過程稱為派生,由已存在的類派生出來的新類稱為派生類,又稱子類。
因為 Dog 繼承了 Animal 的功能,因此我們可以創(chuàng)建一個 Dog 的實例,擁有 bark() 和 move() 方法。
下面我們來看個更加復(fù)雜的例子。
class Animal {
name: string;
constructor(theName: string) {
this.name = theName;
}
move(distanceInMeters: number = 0) {
console.log(`${this.name} moved ${distanceInMeters}m.`);
}
}
class Snake extends Animal {
constructor(name: string) {
super(name);
}
move(distanceInMeters = 5) {
console.log('Slithering');
super.move(distanceInMeters);
}
}
class Horse extends Animal {
constructor(name: string) {
super(name);
}
move(distanceInMeters = 45) {
console.log('Galloping');
super.move(distanceInMeters);
}
}
let sam = new Snake('Sammy the Python');
let tom: Animal = new Horse('Tommy the Palomino');
sam.move(); // Slithering Sammy the Python moved 5m.
tom.move(34); // Galloping Tommy the Palomino moved 34m.
這個例子展示了一些上面沒有提到的特性。這一次我們使用 extends 關(guān)鍵字創(chuàng)建了 Animal 的兩個子類: Snake 和 Horse 。
與前一個例子的不同點是,派生類包含了一個構(gòu)造函數(shù),它必須調(diào)用 super,它會執(zhí)行基類的構(gòu)造函數(shù)。而且,在構(gòu)造函數(shù)里訪問 this 之前,我們一定要調(diào)用 super,這個是 TypeScript 強(qiáng)制執(zhí)行的一條重要規(guī)則。
這個例子演示了如何讓在子類里重寫父類的方法。 Snake 和 Horse 類都創(chuàng)建了 move 方法,它們重寫了從 Animal 繼承來的 move 方法,使得 move 方法根據(jù)不同的類而具有不同的功能。注意,即使 tom 被聲明為 Animal 類型,但因為它是 Horse ,調(diào)用 tom.move(34) 時,它會調(diào)用 Horse 重寫的方法。
公共,私有與受保護(hù)的修飾符
公共 public
在上面的例子里,我們可以自由的訪問程序里定義的成員。如果你對其語言中的類比較了解,就會注意到我們在之前的代碼里并沒有使用 public 來做修飾符,在 TypeScript 里,成員都默認(rèn)為 public 。
你也可以明確的將一個成員標(biāo)記成 public 。我們可以用下面的方式來重寫上面的 Animal 類:
class Animal {
public name: string;
public constructor(theName: string) {
this.name = theName;
}
public move(distanceInMeters: number = 0) {
console.log(`${this.name} moved ${distanceInMeters}m.`);
}
}
私有 private
當(dāng)成員被標(biāo)記為 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'
TypeScript 使用的是結(jié)構(gòu)性類型系統(tǒng)。當(dāng)我們比較兩種不同的類型時,并不在乎它們從何處而來,如果所有成員的類型都是兼容的,我們就認(rèn)為它們的類型是兼容的。
然而,當(dāng)我們比較帶有 private 或 protected 成員類型的時候,情況就不同了。如果其中一個類型里包含 private 成員,那么只有當(dāng)另外一個類型中也存在這樣一個 private 成員,并且它們都是來自同一處聲明時,我們才認(rèn)為兩個類型是兼容的。對于 protected 成員也使用這個規(guī)則。
下面來看一個例子,更好的說明了這一點:
class Animal {
private name: string;
constructor(theName: string) {
this.name = theName;
}
}
class Rhino extends Animal {
constructor() {
super('Rhino');
}
}
class Employee {
private name: string;
consoructor(theName: string) {
this.name = theName;
}
}
let animal = new Animal('Goat');
let rhino = new Rhino();
let employee = new Employee('Bob');
animal = rhino; // okay
animal = employee; // 錯誤:Animal 與 Employee 不兼容.
在這個例子中,有 Animal 、 Rhino 和 Empolyee 三個類, Rhino 是 Animal 的子類, Empolyee 類看上去與 Animal 是相同的。我們創(chuàng)建了幾個這些類的實例,并相互賦值拉看看會發(fā)生什么。因為 Animal 和 Rhino 共享了來自 Animal 里的私有成員定義 private name: string,因此它們是兼容的。然而 Emloyee 卻 不是這樣。當(dāng)把 Employee 賦值給 Animal 的時候,得到一個錯誤,說它們的類型不兼容。盡管 Employee 里也有一個私有成員 name ,但它明顯不是 Animal 里面定義的那個。
受保護(hù)的 protected
protected 修飾符與 private 修飾符的行為很相似,但有一點不同, protected 成員在派生類中仍然可以訪問。例如:
class Person {
protected name: string;
constructor(theName: string) {
this.name = theName;
}
}
class Empolyee extends Person {
private department: string;
constructor(theName: string, department: string) {
super(theName);
this.department = department;
}
public getElevatorPitch() {
return `Hello, my name is ${this.name} and I work in ${this.department}.`;
}
}
let howard = new Empolyee('Howard', 'Sales');
console.log(howard);
// {name: 'Howard', department: 'Sales'}
console.log(howard.getElevatorPitch());
// Hello, my name is Howard and I work in Sales.
console.log(howard.name);
// 錯誤:屬性 'name' 受保護(hù),并且只能在類 'Person' 及其子類中訪問
注意 ??,我們不能再 Person 類外使用 name,但是我們?nèi)匀豢梢酝ㄟ^ Empolyee 類的實例方法訪問,因為 Employee 是由 Person 派生而來的。
構(gòu)造函數(shù)可以被標(biāo)記成 protected,這意味著這個類不能在包含它的類外被實例化,但是能被繼承。比如:
class Person {
protected name: string;
protected constructor(theName) {
this.name = theName;
}
}
// Empooyee 能繼承 Person
class Employee extends Person {
private department: string;
constructor(theName: string, department: string) {
super(theName);
this.department = department;
}
public getElevatorPitch() {
return `Hello, my name is ${this.name} and I work in ${this.department}.`;
}
}
let howard = new Employee('Howard', 'Sales'); // okay
let john = new Person('John'); // 錯誤:Person 的構(gòu)造函數(shù)是被保護(hù)的
reanonly 修飾符
你可以使用 readonly 關(guān)鍵字將屬性設(shè)置為只讀的。只讀屬性必須在聲明時或構(gòu)造函數(shù)里被初始化。
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');
console.log(dad);
// {numberOfLegs: 8, name: 'Man with the 8 strong legs'}
dad.name = 'Man with the 3-piece suit';
// 錯誤:name 是只讀的.
參數(shù)屬性
在上面的例子中,我們必須在 Octopus 類里定義一個只讀成員 name 和 theName 的構(gòu)造函數(shù),并且立刻將 theName 賦值給 name,這種情況經(jīng)常會遇到。參數(shù)屬性可以方便的讓我們在一個地方定義并初始化一個成員。下面的例子是對之前 Octopus 的修改版,使用了參數(shù)屬性:
class Octopus {
readonly numberOfLegs: number = 8;
constructor(readonly name: string) {}
}
注意看我們是如何舍棄了 theName,僅在構(gòu)造函數(shù)里使用 readonly name: string,參數(shù)來創(chuàng)建和初始化 name 成員。我們把聲明和賦值合并至一處。
參數(shù)屬性通過構(gòu)造函數(shù)參數(shù)前面添加一個訪問限定符來聲明。使用 private 限定一個參數(shù)屬性會聲明并初始化一個私有成員;對于 public 和 protected 來說也是一樣。
存取器
TypeScript 支持通過 getter 和 setter 來截取對對象成員的訪問。它能幫助你有效的控制對對象成員的訪問。
下面來看如何把一個簡單的類改寫成使用 get 和 set 。首先,我們從一個沒有使用存取器的例子開始。
class Employee {
fullName: string;
}
let employee = new Employee();
employee.fullName = 'Bob Smith';
if (employee.fullName) {
console.log(employee.fullName); // Bob Smith
}
我們可以隨意的設(shè)置 fullName,這是非常方便的,但是這也可能會帶來麻煩。
下面這個版本里,我們先檢查用戶密碼是否正確,然后在允許其修改員工信息。我們把 fullName 的直接訪問改成了可以檢查密碼的 set 方法。我們也加了一個 get 方法,讓上面的例子仍然可以工作。
let passcode = 'secret passcode';
class Employee {
private _fullName: string;
get fullName(): string {
return this._fullName;
}
set fullName(newName: string) {
if (passcode && passcode !== 'secret passcode') {
this._fullName = newName;
} else {
console.log('Error:Unauthorized update of employee!');
}
}
}
let employee = new Employee();
employee.fullName = 'Bob Smith';
if (employee.fullName) {
alert(employee.fullName); // 彈出 Blob Smith
}
我們可以修改一下密碼,來驗證一下存取器是否工作的。當(dāng)密碼不對時,會提示我們沒有權(quán)限去修改員工。
對于存取器有下面幾點需要注意的:
- 首先,存取器要求你將編譯器設(shè)置為輸出
ECMAScript 5或更高,不支持降級到ECMAScript 3。 - 其次,只有帶
get不帶有set的存取器自動被推斷為readonly。
靜態(tài)屬性
到目前為止,我們只討論了類的實例成員,那些僅當(dāng)類被實例化的時候才會被初始化的屬性。我們可以創(chuàng)建類的靜態(tài)成員,這些屬性存在于類本身上面而不是類的實例上。在這個例子里,我們使用 static 定義 origin,因為它是所有網(wǎng)格都會用到的屬性。每個實例想要訪問這個屬性的時候,都要在 origin 前加上類名。如同在實例屬性上使用 this. 前綴來訪問屬性一樣,這里我們使用 Grid. 來訪問靜態(tài)屬性。
class Grid {
static origin = { x: 0, y: 0 };
calculateDistanceFromOrigin(point: { x: number; y: number }) {
let xDist = point.x - Grid.origin.x;
let yDist = point.x - Grid.origin.y;
return Math.sqrt(xDist * xDist + yDist * yDist) / this.scale;
}
constructor(public scale: number) {}
}
let grid1 = new Grid(1.0); // 5x scale
let grid2 = new Grid(5.0); // 5x scale
console.log(grid1.calculateDistanceFromOrigin({ x: 10, y: 10 }));
// 14.142135623730951
console.log(grid2.calculateDistanceFromOrigin({ x: 10, y: 10 }));
// 2.8284271247461903
抽象類
抽象類作為其他派生類的基類使用。它們一般不會直接被實例化。不同于接口,抽象類可以包含成員的實現(xiàn)細(xì)節(jié)。abstract 關(guān)鍵字是用于定義抽象類和在抽象類內(nèi)部定義抽象方法。
abstract class Animal {
abstract makeSound(): void;
move(): void {
console.log('roaming the search...');
}
}
抽象類中的抽象方法不包含具體實現(xiàn)并且必須在派生類中實現(xiàn)。抽象方法的語法于與接口方法類似。兩者都是定義方法簽名但不包含方法體。然而,抽象方法必須包含 abstract 關(guān)鍵字并且可以包含訪問修飾符。
abstract class Department {
constructor(public name: string) {}
printName(): void {
console.log('Department name ' + this.name);
}
abstract printMeeting(): void; // 必須在派生類中實現(xiàn)
}
class AccountingDepartment extends Department {
constructor() {
super('Accounting and Auditing');
// 在派生類的構(gòu)造函數(shù)中必須調(diào)用 super()
}
printMeeting(): void {
console.log('The Accounting Department meets each Monday at 10am.');
}
generateReports(): void {
console.log('Genaerating accounting reports...');
}
}
let department: Department; // 允許創(chuàng)建一個對抽象類型的引用
department = new Department();
// error:Cannot create an instance of an abstract class.
// 錯誤:不能創(chuàng)建一個抽象類的實例
department = new AccountingDepartment();
// 允許對一個抽象子類進(jìn)行實例化和賦值
department.printName();
// Department name Accounting and Auditing
department.printMeeting();
// The Accounting Department meets each Monday at 10am.'
department.generateReports();
// error: Property 'generateReports' does not exist on type 'Department'.
// generateReports 方法在聲明的抽象類中不存在
高級技巧
構(gòu)造函數(shù)
當(dāng)你在 TypeScript 中聲明了一個類的時候,實際上同時聲明了很多東西。首先就是 實例的類型。
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return 'Hello,' + this.greeting;
}
}
let greeter: Greeter;
greeter = new Greeter('World');
console.log(greeter.greet()); // Hello,World
這里我們寫了 let greeter: Greeter,意思是 Greeter 類的實例類型是 Greeter 。這對于用過其他面向?qū)ο蟮恼Z言的程序員來說講已經(jīng)是老習(xí)慣了。
我們也創(chuàng)建了一個叫構(gòu)造函數(shù)的值。這個函數(shù)會在我們使用 new 創(chuàng)建類實例的時候被調(diào)用。下面我們來看看,上面的代碼被編譯成 JavaScript 后是什么樣子的。
var Greeter = /** @class */ (function() {
function Greeter(message) {
this.greeting = message;
}
Greeter.prototype.greet = function() {
return 'Hello,' + this.greeting;
};
return Greeter;
})();
var greeter;
greeter = new Greeter('World');
console.log(greeter.greet()); // Hello,World
上面的代碼里,var Greeter 將被賦值為構(gòu)造函數(shù)。當(dāng)我們調(diào)用 new 并執(zhí)行了這個函數(shù)后,便會得到一個類的實例。這個構(gòu)造函數(shù)也包含了類的所有靜態(tài)屬性。換個角度說,我們可以認(rèn)為類具有實例部分和靜態(tài)部分這個兩個部分。
讓我們稍微改寫一下這個例子,看看它們之間的區(qū)別:
class Greeter {
static standardGreeting = 'Hello,there';
greeting: string;
greet() {
if (this.greeting) {
return 'Hello,' + this.greeting;
} else {
return Greeter.standardGreeting;
}
}
}
let greeter1: Greeter;
greeter1 = new Greeter();
console.log(greeter1.greet()); // Hello,there
let greeterMaker: typeof Greeter = Greeter;
greeterMaker.standardGreeting = 'Hey,there';
let greeter2: Greeter;
greeter2 = new Greeter();
console.log(greeter2.greet()); // Hey,there
這個例子里, greeter1 與之前看到的一樣。我們實例化 Greeter 類,并使用這個對象。與我們之前看到的一樣。
再之后,我們直接使用類。我們創(chuàng)建了一個叫 greeterMaker 的變量。這個變量保存了這個類或者說保存了類構(gòu)造函數(shù)。然后我們使用 typeof Greeter,意思是取 Greeter 類的類型,而不是實例的類型?;蛘吒_切的說,"告訴我 Greeter 標(biāo)識符的類型",也就是構(gòu)造函數(shù)的類型。這個類型包含了類的所有靜態(tài)成員和構(gòu)造函數(shù)。之后,就和前面一樣,我們在 greeterMaker 上使用 new,創(chuàng)建 Greeter 的實例。
把類當(dāng)做接口使用
類定義會創(chuàng)建兩個東西:類的實例類型和一個構(gòu)造函數(shù)。因為類可以創(chuàng)建出類型,所以你能夠在允許使用接口的地方使用類。
class Point {
x: number;
y: number;
}
interface Point3d extends Point {
z: number;
}
let point3d: Point3d = { x: 1, y: 2, z: 3 };
console.log(point3d); // { x: 1, y: 2, z: 3 }
本文參考來源: TypeScript 類