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