傳統(tǒng)的JavaScript的程序使用函數(shù)和基于原型的繼承來(lái)創(chuàng)建可重用的組件,但對(duì)于熟悉使用面向?qū)ο蠓绞降某绦騿T來(lái)講就有些棘手,因?yàn)樗麄冇玫氖腔陬?lèi)的繼承并且對(duì)象是由類(lèi)構(gòu)建出來(lái)從ECMAScript 2015,也就是ECMAScript 6開(kāi)始,JavaScript程序員將能夠使用基于類(lèi)的面向?qū)ο蟮姆绞?。使用TypeScript,我們?cè)试S開(kāi)發(fā)者現(xiàn)在就使用這些特性,并且編譯后的JavaScript可以在所有主流瀏覽器和平臺(tái)上運(yùn)行,而不需要等到下個(gè)JavaScript的版本。
下面看一個(gè)使用類(lèi)的例子:
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}
let greeter = new Greeter("world");
如果你使用過(guò)C?;騄ava,你會(huì)對(duì)這種語(yǔ)法非常熟悉。我們聲明一個(gè) Greeter
類(lèi)。這個(gè)類(lèi)有3個(gè)成員:一個(gè)叫做greeting
的屬性,一個(gè)構(gòu)造函數(shù)和一個(gè)greet
方法。
你會(huì)注意到,我們?cè)谝萌魏我粋€(gè)類(lèi)成員的時(shí)候都用了this
。它表示我們?cè)L問(wèn)的是類(lèi)的成員。
最后一行,我們使用new
構(gòu)造了Greeter
類(lèi)的一個(gè)實(shí)例。它會(huì)調(diào)用之前定義的構(gòu)造函數(shù),創(chuàng)建一個(gè) Greeter
類(lèi)型的新對(duì)象,并執(zhí)行構(gòu)造函數(shù)初始化它。
在TypeScript里,我們可以使用常用的面向?qū)ο竽J健.?dāng)然,基于類(lèi)的程序設(shè)計(jì)中最基本的模式是允許使用繼承來(lái)擴(kuò)展現(xiàn)有的類(lèi)。
看下面的例子:
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();
tom.move(34);
這個(gè)例子展示了TypeScript中繼承的一些特征,它們與其它語(yǔ)言類(lèi)似。 我們使用 extends
關(guān)鍵字來(lái)創(chuàng)建子類(lèi)。你可以看到Horse
和Snake
類(lèi)是基類(lèi)Animal
的子類(lèi),并且可以訪(fǎng)問(wèn)其屬性和方法。
包含構(gòu)造函數(shù)的派生類(lèi)必須調(diào)用super()
,它會(huì)執(zhí)行基類(lèi)的構(gòu)造方法。
這個(gè)例子演示了如何在子類(lèi)里可以重寫(xiě)父類(lèi)的方法。 Snake
類(lèi)和Horse
類(lèi)都創(chuàng)建了move
方法,它們重寫(xiě)了從Animal
繼承來(lái)的move
方法,使得move
方法根據(jù)不同的類(lèi)而具有不同的功能。 注意,即使 tom
被聲明為Animal
類(lèi)型,但因?yàn)樗闹凳?code>Horse,tom.move(34)
會(huì)調(diào)用Horse
里的重寫(xiě)方法:
Slithering...
Sammy the Python moved 5m.
Galloping...
Tommy the Palomino moved 34m.
public
在上面的例子里,我們可以自由的訪(fǎng)問(wèn)程序里定義的成員。 如果你對(duì)其它語(yǔ)言中的類(lèi)比較了解,就會(huì)注意到我們?cè)谥暗拇a里并沒(méi)有使用 public
來(lái)做修飾;例如,C#要求必須明確地使用public
指定成員是可見(jiàn)的。 在TypeScript里,成員都默認(rèn)為 public
。
你也可以明確的將一個(gè)成員標(biāo)記成public
。 我們可以用下面的方式來(lái)重寫(xiě)上面的 Animal
類(lèi):
class Animal {
public name: string;
public constructor(theName: string) { this.name = theName; }
public move(distanceInMeters: number) {
console.log(`${this.name} moved ${distanceInMeters}m.`);
}
}
private
當(dāng)成員被標(biāo)記成private
時(shí),它就不能在聲明它的類(lèi)的外部訪(fǎng)問(wèn)。比如:
class Animal {
private name: string;
constructor(theName: string) { this.name = theName; }
}
new Animal("Cat").name; // Error: 'name' is private;
TypeScript使用的是結(jié)構(gòu)性類(lèi)型系統(tǒng)。 當(dāng)我們比較兩種不同的類(lèi)型時(shí),并不在乎它們從何處而來(lái),如果所有成員的類(lèi)型都是兼容的,我們就認(rèn)為它們的類(lèi)型是兼容的。
然而,當(dāng)我們比較帶有private
或protected
成員的類(lèi)型的時(shí)候,情況就不同了。 如果其中一個(gè)類(lèi)型里包含一個(gè) private
成員,那么只有當(dāng)另外一個(gè)類(lèi)型中也存在這樣一個(gè)private
成員, 并且它們都是來(lái)自同一處聲明時(shí),我們才認(rèn)為這兩個(gè)類(lèi)型是兼容的。 對(duì)于 protected
成員也使用這個(gè)規(guī)則。
下面來(lái)看一個(gè)例子,更好地說(shuō)明了這一點(diǎn):
class Animal {
private name: string;
constructor(theName: string) { this.name = theName; }
}
class Rhino extends Animal {
constructor() { super("Rhino"); }
}
class Employee {
private name: string;
constructor(theName: string) { this.name = theName; }
}
let animal = new Animal("Goat");
let rhino = new Rhino();
let employee = new Employee("Bob");
animal = rhino;
animal = employee; // Error: Animal and Employee are not compatible
這個(gè)例子中有Animal
和Rhino
兩個(gè)類(lèi),Rhino
是Animal
類(lèi)的子類(lèi)。 還有一個(gè) Employee
類(lèi),其類(lèi)型看上去與Animal
是相同的。 我們創(chuàng)建了幾個(gè)這些類(lèi)的實(shí)例,并相互賦值來(lái)看看會(huì)發(fā)生什么。 因?yàn)?nbsp;Animal
和Rhino
共享了來(lái)自Animal
里的私有成員定義private name: string
,因此它們是兼容的。 然而 Employee
卻不是這樣。當(dāng)把Employee
賦值給Animal
的時(shí)候,得到一個(gè)錯(cuò)誤,說(shuō)它們的類(lèi)型不兼容。 盡管 Employee
里也有一個(gè)私有成員name
,但它明顯不是Animal
里面定義的那個(gè)。
protected
protected
修飾符與private
修飾符的行為很相似,但有一點(diǎn)不同,protected
成員在派生類(lèi)中仍然可以訪(fǎng)問(wèn)。例如:
class Person {
protected name: string;
constructor(name: string) { this.name = name; }
}
class Employee extends Person {
private department: string;
constructor(name: string, department: string) {
super(name)
this.department = department;
}
public getElevatorPitch() {
return `Hello, my name is ${this.name} and I work in ${this.department}.`;
}
}
let howard = new Employee("Howard", "Sales");
console.log(howard.getElevatorPitch());
console.log(howard.name); // error
注意,我們不能在Person
類(lèi)外使用name
,但是我們?nèi)匀豢梢酝ㄟ^(guò)Employee
類(lèi)的實(shí)例方法訪(fǎng)問(wèn),因?yàn)?code>Employee是由Person
派生而來(lái)的。
構(gòu)造函數(shù)也可以被標(biāo)記成protected
。 這意味著這個(gè)類(lèi)不能在包含它的類(lèi)外被實(shí)例化,但是能被繼承。比如,
class Person {
protected name: string;
protected constructor(theName: string) { this.name = theName; }
}
// Employee can extend Person
class Employee extends Person {
private department: string;
constructor(name: string, department: string) {
super(name);
this.department = department;
}
public getElevatorPitch() {
return `Hello, my name is ${this.name} and I work in ${this.department}.`;
}
}
let howard = new Employee("Howard", "Sales");
let john = new Person("John"); // Error: The 'Person' constructor is protected
你可以使用readonly
關(guān)鍵字將屬性設(shè)置為只讀的。 只讀屬性必須在聲明時(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");
dad.name = "Man with the 3-piece suit"; // error! name is readonly.
在上面的例子中,我們不得不定義一個(gè)受保護(hù)的成員name
和一個(gè)構(gòu)造函數(shù)參數(shù)theName
在Person
類(lèi)里,并且立刻給name
和theName
賦值。 這種情況經(jīng)常會(huì)遇到。 參數(shù)屬性可以方便地讓我們?cè)谝粋€(gè)地方定義并初始化一個(gè)成員。 下面的例子是對(duì)之前 Animal
類(lèi)的修改版,使用了參數(shù)屬性:
class Animal {
constructor(private name: string) { }
move(distanceInMeters: number) {
console.log(`${this.name} moved ${distanceInMeters}m.`);
}
}
注意看我們是如何舍棄了theName
,僅在構(gòu)造函數(shù)里使用private name: string
參數(shù)來(lái)創(chuàng)建和初始化name
成員。 我們把聲明和賦值合并至一處。
參數(shù)屬性通過(guò)給構(gòu)造函數(shù)參數(shù)添加一個(gè)訪(fǎng)問(wèn)限定符來(lái)聲明。 使用 private
限定一個(gè)參數(shù)屬性會(huì)聲明并初始化一個(gè)私有成員;對(duì)于public
和protected
來(lái)說(shuō)也是一樣。
TypeScript支持通過(guò)getters/setters來(lái)截取對(duì)對(duì)象成員的訪(fǎng)問(wèn)。 它能幫助你有效的控制對(duì)對(duì)象成員的訪(fǎng)問(wèn)。
下面來(lái)看如何把一個(gè)簡(jiǎn)單的類(lèi)改寫(xiě)成使用get
和set
。 首先,我們從一個(gè)沒(méi)有使用存取器的例子開(kāi)始。
class Employee {
fullName: string;
}
let employee = new Employee();
employee.fullName = "Bob Smith";
if (employee.fullName) {
console.log(employee.fullName);
}
我們可以隨意的設(shè)置fullName
,這是非常方便的,但是這也可能會(huì)帶來(lái)麻煩。
下面這個(gè)版本里,我們先檢查用戶(hù)密碼是否正確,然后再允許其修改員工信息。 我們把對(duì) fullName
的直接訪(fǎng)問(wèn)改成了可以檢查密碼的set
方法。 我們也加了一個(gè) 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);
}
我們可以修改一下密碼,來(lái)驗(yàn)證一下存取器是否是工作的。當(dāng)密碼不對(duì)時(shí),會(huì)提示我們沒(méi)有權(quán)限去修改員工。
對(duì)于存取器有下面幾點(diǎn)需要注意的:
首先,存取器要求你將編譯器設(shè)置為輸出ECMAScript 5或更高。 不支持降級(jí)到ECMAScript 3。 其次,只帶有 get
不帶有set
的存取器自動(dòng)被推斷為readonly
。 這在從代碼生成 .d.ts
文件時(shí)是有幫助的,因?yàn)槔眠@個(gè)屬性的用戶(hù)會(huì)看到不允許夠改變它的值。
到目前為止,我們只討論了類(lèi)的實(shí)例成員,那些僅當(dāng)類(lèi)被實(shí)例化的時(shí)候才會(huì)被初始化的屬性。 我們也可以創(chuàng)建類(lèi)的靜態(tài)成員,這些屬性存在于類(lèi)本身上面而不是類(lèi)的實(shí)例上。 在這個(gè)例子里,我們使用 static
定義origin
,因?yàn)樗撬芯W(wǎng)格都會(huì)用到的屬性。 每個(gè)實(shí)例想要訪(fǎng)問(wèn)這個(gè)屬性的時(shí)候,都要在 origin
前面加上類(lèi)名。 如同在實(shí)例屬性上使用 this.
前綴來(lái)訪(fǎng)問(wèn)屬性一樣,這里我們使用Grid.
來(lái)訪(fǎng)問(wèn)靜態(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.y - Grid.origin.y);
return Math.sqrt(xDist * xDist + yDist * yDist) / this.scale;
}
constructor (public scale: number) { }
}
let grid1 = new Grid(1.0); // 1x scale
let grid2 = new Grid(5.0); // 5x scale
console.log(grid1.calculateDistanceFromOrigin({x: 10, y: 10}));
console.log(grid2.calculateDistanceFromOrigin({x: 10, y: 10}));
抽象類(lèi)做為其它派生類(lèi)的基類(lèi)使用。 它們一般不會(huì)直接被實(shí)例化。 不同于接口,抽象類(lèi)可以包含成員的實(shí)現(xiàn)細(xì)節(jié)。abstract
關(guān)鍵字是用于定義抽象類(lèi)和在抽象類(lèi)內(nèi)部定義抽象方法。
abstract class Animal {
abstract makeSound(): void;
move(): void {
console.log('roaming the earch...');
}
}
抽象類(lèi)中的抽象方法不包含具體實(shí)現(xiàn)并且必須在派生類(lèi)中實(shí)現(xiàn)。 抽象方法的語(yǔ)法與接口方法相似。 兩者都是定義方法簽名但不包含方法體。 然而,抽象方法必須包含 abstract
關(guān)鍵字并且可以包含訪(fǎng)問(wèn)修飾符。
abstract class Department {
constructor(public name: string) {
}
printName(): void {
console.log('Department name: ' + this.name);
}
abstract printMeeting(): void; // 必須在派生類(lèi)中實(shí)現(xiàn)
}
class AccountingDepartment extends Department {
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...');
}
}
let department: Department; // ok to create a reference to an abstract type
department = new Department(); // error: cannot create an instance of an abstract class
department = new AccountingDepartment(); // ok to create and assign a non-abstract subclass
department.printName();
department.printMeeting();
department.generateReports(); // error: method doesn't exist on declared abstract type
當(dāng)你在TypeScript里聲明了一個(gè)類(lèi)的時(shí)候,實(shí)際上同時(shí)聲明了很多東西。 首先就是類(lèi)的 實(shí)例的類(lèi)型。
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());
這里,我們寫(xiě)了let greeter: Greeter
,意思是Greeter
類(lèi)的實(shí)例的類(lèi)型是Greeter
。 這對(duì)于用過(guò)其它面向?qū)ο笳Z(yǔ)言的程序員來(lái)講已經(jīng)是老習(xí)慣了。
我們也創(chuàng)建了一個(gè)叫做構(gòu)造函數(shù)的值。這個(gè)函數(shù)會(huì)在我們使用 new
創(chuàng)建類(lèi)實(shí)例的時(shí)候被調(diào)用。下面我們來(lái)看看,上面的代碼被編譯成JavaScript后是什么樣子的:
let Greeter = (function () {
function Greeter(message) {
this.greeting = message;
}
Greeter.prototype.greet = function () {
return "Hello, " + this.greeting;
};
return Greeter;
})();
let greeter;
greeter = new Greeter("world");
console.log(greeter.greet());
上面的代碼里,let Greeter
將被賦值為構(gòu)造函數(shù)。我們當(dāng)調(diào)用 new
并執(zhí)行了這個(gè)函數(shù)后,便會(huì)得到一個(gè)類(lèi)的實(shí)例。這個(gè)構(gòu)造函數(shù)也包含了類(lèi)的所有靜態(tài)屬性。換個(gè)角度說(shuō),我們可以認(rèn)為類(lèi)具有 實(shí)例部分與靜態(tài)部分這兩個(gè)部分。
讓我們稍微改寫(xiě)一下這個(gè)例子,看看它們之前的區(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());
let greeterMaker: typeof Greeter = Greeter;
greeterMaker.standardGreeting = "Hey there!";
let greeter2: Greeter = new greeterMaker();
console.log(greeter2.greet());
這個(gè)例子里,greeter1
與之前看到的一樣。我們實(shí)例化 Greeter
類(lèi),并使用這個(gè)對(duì)象。與我們之前看到的一樣。
再之后,我們直接使用類(lèi)。我們創(chuàng)建33了一個(gè)叫做 greeterMaker
的變量。這個(gè)變量保存了這個(gè)類(lèi)或者說(shuō)保存了類(lèi)構(gòu)造函數(shù)。然后我們使用 typeof Greeter
,意思是取Greeter類(lèi)的類(lèi)型,而不是實(shí)例的類(lèi)型?;蛘吒_切的說(shuō), “我告訴 Greeter
標(biāo)識(shí)符的類(lèi)型”,也就是構(gòu)造函數(shù)的類(lèi)型。這個(gè)類(lèi)型包含了類(lèi)的所有靜態(tài)成員和構(gòu)造函數(shù)。之后,就和前面一樣,在我們 greeterMaker
上使用new
,創(chuàng)建33 Greeter
的實(shí)例。
如上一節(jié)里所講的,類(lèi)定義會(huì)創(chuàng)建兩個(gè)東西:類(lèi)的實(shí)例類(lèi)型和一個(gè)構(gòu)造函數(shù)。因?yàn)轭?lèi)可以創(chuàng)建出類(lèi)型,所以你能夠在允許使用接口的地方使用類(lèi)。
class Point {
x: number;
y: number;
}
interface Point3d extends Point {
z: number;
}
let point3d: Point3d = {x: 1, y: 2, z: 3};
更多建議: