傳統(tǒng)的JavaScript程序使用函數(shù)和基于原型的繼承來創(chuàng)建可重用的組件,但這對(duì)于熟悉使用面向?qū)ο蠓绞降某绦騿T來說有些棘手,因?yàn)樗麄冇玫氖腔陬惖睦^承并且對(duì)象是從類構(gòu)建出來的。 從ECMAScript 2015,也就是ECMAScript 6,JavaScript程序?qū)⒖梢允褂眠@種基于類的面向?qū)ο蠓椒ā?在TypeScript里,我們?cè)试S開發(fā)者現(xiàn)在就使用這些特性,并且編譯后的JavaScript可以在所有主流瀏覽器和平臺(tái)上運(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#或Java,你會(huì)對(duì)這種語法非常熟悉。
我們聲明一個(gè)Greeter
類。這個(gè)類有3個(gè)成員:一個(gè)叫做greeting
的屬性,一個(gè)構(gòu)造函數(shù)和一個(gè)greet
方法。
你會(huì)注意到,我們?cè)谝萌魏我粋€(gè)類成員的時(shí)候都用了this
。
它表示我們?cè)L問的是類的成員。
最后一行,我們使用new
構(gòu)造了Greeter類的一個(gè)實(shí)例。
它會(huì)調(diào)用之前定義的構(gòu)造函數(shù),創(chuàng)建一個(gè)Greeter
類型的新對(duì)象,并執(zhí)行構(gòu)造函數(shù)初始化它。
在TypeScript里,我們可以使用常用的面向?qū)ο竽J健?當(dāng)然,基于類的程序設(shè)計(jì)中最基本的模式是允許使用繼承來擴(kuò)展一個(gè)類。
看下面的例子:
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
來創(chuàng)建子類。你可以看到Horse
和Snake
類是基類Animal
的子類,并且可以訪問其屬性和方法。
包含constructor函數(shù)的派生類必須調(diào)用super()
,它會(huì)執(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.
在上面的例子里,我們可以自由的訪問程序里定義的成員。
如果你對(duì)其它語言中的類比較了解,就會(huì)注意到我們?cè)谥暗拇a里并沒有使用public
來做修飾;例如,C#要求必須明確地使用public
指定成員是可見的。
在TypeScript里,每個(gè)成員默認(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è)類型是兼容的。
對(duì)于protected
成員也使用這個(gè)規(guī)則。
下面來看一個(gè)例子,詳細(xì)的解釋了這點(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í)例,并相互賦值來看看會(huì)發(fā)生什么。
因?yàn)?code>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è)受保護(hù)的成員name
和一個(gè)構(gòu)造函數(shù)參數(shù)theName
在Person
類里,并且立刻給name
和theName
賦值。
這種情況經(jīng)常會(huì)遇到。參數(shù)屬性可以方便地讓我們?cè)谝粋€(gè)地方定義并初始化一個(gè)成員。
下面的例子是對(duì)之前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ù)屬性會(huì)聲明并初始化一個(gè)私有成員;對(duì)于public
和protected
來說也是一樣。
TypeScript支持getters/setters來截取對(duì)對(duì)象成員的訪問。 它能幫助你有效的控制對(duì)對(duì)象成員的訪問。
下面來看如何把一類改寫成使用get
和set
。
首先是一個(gè)沒用使用存取器的例子。
class Employee {
fullName: string;
}
let employee = new Employee();
employee.fullName = "Bob Smith";
if (employee.fullName) {
console.log(employee.fullName);
}
我們可以隨意的設(shè)置fullName
,這是非常方便的,但是這也可能會(huì)帶來麻煩。
下面這個(gè)版本里,我們先檢查用戶密碼是否正確,然后再允許其修改employee。
我們把對(duì)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)密碼不對(duì)時(shí),會(huì)提示我們沒有權(quán)限去修改employee。
注意:若要使用存取器,要求設(shè)置編譯器輸出目標(biāo)為ECMAScript 5或更高。
到目前為止,我們只討論了類的實(shí)例成員,那些僅當(dāng)類被實(shí)例化的時(shí)候才會(huì)被初始化的屬性。
我們也可以創(chuàng)建類的靜態(tài)成員,這些屬性存在于類本身上面而不是類的實(shí)例上。
在這個(gè)例子里,我們使用static
定義origin
,因?yàn)樗撬芯W(wǎng)格都會(huì)用到的屬性。
每個(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}));
抽象類是供其它類繼承的基類。
他們一般不會(huì)直接被實(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里定義類的時(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
。
這對(duì)于用過其它面向?qū)ο笳Z言的程序員來講已經(jīng)是老習(xí)慣了。
我們也創(chuàng)建了一個(gè)叫做構(gòu)造函數(shù)的值。
這個(gè)函數(shù)會(huì)在我們使用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)我們使用new
并執(zhí)行這個(gè)函數(shù)后,便會(huì)得到一個(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è)對(duì)象。
與我們之前看到的一樣。
再之后,我們直接使用類。
我們創(chuàng)建了一個(gè)叫做greeterMaker
的變量。
這個(gè)變量保存了這個(gè)類或者說保存了類構(gòu)造函數(shù)。
然后我們使用typeof Greeter
,意思是取Greeter類的類型,而不是實(shí)例的類型。
或者更確切的說,"告訴我Greeter
標(biāo)識(shí)符的類型",也就是構(gòu)造函數(shù)的類型。
這個(gè)類型包含了類的所有靜態(tài)成員和構(gòu)造函數(shù)。
之后,就和前面一樣,我們?cè)?code>greeterMaker上使用new
,創(chuàng)建Greeter
的實(shí)例。
如上一節(jié)里所講的,類定義會(huì)創(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};