傳統的JavaScript註重用函數和基於原型的繼承來創建可復用的組件,但這可能讓用習慣面對對象方式的程式員感到棘手,因為他們的繼承和創建對象都是由類而來的。從JavaScript的下一個版本,ECMAScript 6開始,JavaScript程式員就能夠用基於這種基於類的面對對象方式來創建編寫自己
傳統的JavaScript註重用函數和基於原型的繼承來創建可復用的組件,但這可能讓用習慣面對對象方式的程式員感到棘手,因為他們的繼承和創建對象都是由類而來的。從JavaScript的下一個版本,ECMAScript 6開始,JavaScript程式員就能夠用基於這種基於類的面對對象方式來創建編寫自己的程式了。在TypeScript中,不需要再等JavaScript的下一個版本就已經支持開發者使用這一技術了。
類
讓我們來看一個簡單的基於類的例子:
class Greeter { greeting: string; constructor(message: string) { this.greeting = message; } greet() { return "Hello, " + this.greeting; } } var greeter = new Greeter("world");
如果你之前有使用過C#或者Java,會覺得語法非常相似。我們聲明一個新的類"Greeter"。這個類裡面有三個成員,一個名為"greeting"的屬性,一個constructor和一個"greet"方法。
你會註意到,在類裡面當某一個成員使用了"this",意味著他訪問的是這個類的成員。
在最後一行中,我們使用"new"來為Greeter類構造一個實例。這將會調用之前定義的構造函數,並且創建一個新的Greeter類型的對象,並且執行構造函數來初始化這個對象。
繼承
在TypeScript中,我們可以使用常見的面向對象模式。當然,在基於類的編程中最基本的模式之一就是能夠創建一個新的類,這個新的類繼承已有的類,並對已有的類做擴展。
來看一個例子:
class Animal { name:string; constructor(theName: string) { this.name = theName; } move(meters: number = 0) { alert(this.name + " moved " + meters + "m."); } } class Snake extends Animal { constructor(name: string) { super(name); } move(meters = 5) { alert("Slithering..."); super.move(meters); } } class Horse extends Animal { constructor(name: string) { super(name); } move(meters = 45) { alert("Galloping..."); super.move(meters); } } var sam = new Snake("Sammy the Python"); var tom: Animal = new Horse("Tommy the Palomino"); sam.move(); tom.move(34);
這個例子包含了TypeScript中繼承的特性,當然,在其他語言中也一樣。在這裡,我們使用"extends"關鍵字來創建一個子類。你可以看到,這裡"Horse"和"Snake"兩個子類都基於"Animal"這個父類,並且對其特性進行了擴展。在這裡,我們使用"extends"關鍵字來創建一個子類。你可以看到,這裡"Horse"和"Snake"兩個子類都基於"Animal"這個基類並且獲取其特性。
例子也提現出在子類中可以重寫基類中的方法以達到重寫後的方法是在這個子類中專用。這裡的"Horse"和"Snake"都創建了"move"這個方法,這樣就重寫了從基類繼承過來的move方法,並且在不同類中給"move"不同的方法。
公有和私有的修飾符
預設是public(公有)
你可能已經註意到了,在上面的例子中,我們並未對類的任何可見成員使用"public"關鍵字進行修飾。類似C#語言,需要給成員使用"public"修飾符用來明確它是可見。在TypeScript中,每個成員預設是"public"的。
你還可以給成員標記上"private",這樣你就可以控制在你的類之外哪些成員是可見。我們可以像這樣重寫上一節的"Animal"類:
class Animal { private name:string; constructor(theName: string) { this.name = theName; } move(meters: number) { alert(this.name + " moved " + meters + "m."); } }
理解Private(私有)
TypeScript是個構造類型的系統。當我們對兩個類型進行比較的時候,無論它們是從哪裡來,如果所有成員的類型都是相容的,那麼我們可以認為他們的類型也是相容的。
當我們比較的類型中含有"private"(私有)成員,則我們就需要不同的對待了。兩個類型(假如是A和B)被認為是相容的,如果A類型含有一個私有成員,那麼B類型就必須也有一個私有成員並且與A類型的私有成員源自同一處聲明。
讓我們用一個例子來更好的看看私有成員在實踐中如何運用:
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; } } var animal = new Animal("Goat"); var rhino = new Rhino(); var employee = new Employee("Bob"); animal = rhino; animal = employee; // 錯誤: Animal 和 Employee 不相容
在這個例子中,我們有一個"Animal"和一個"Rhino","Rhino"是"Animal"的一個子類。我們還有一個新的類"Employee",它看上去跟"Animal"類是完全相同的。我們給這些類分別創建實例,並且對他們進行相互賦值,看下將會發生什麼。因為"animal"和"rhino"的私有成員都是從"Animal"類定義的"private name: string"共用而來的,所以他們是相容的。然而,"employee"的情況卻不是這樣的。當我們試圖將"employee"賦值給"animal",我們得到了一個錯誤,他們的類型是不相容的。儘管"Employee"也有一個名稱是"name"的私有成員,但它和在"Animal"中的私有成員"name"還是不相同的。
參數屬性
關鍵字"public"和"private"通過創建參數屬性的方式給我們提供了創建和初始化類的成員的便捷方式。這個特性讓你可以一個步驟就創建和初始化成員。這裡有一個之前例子的進一步修改。註意我們是如何在constructor中將"name"使用"private name: string"的便捷方式完整的創建並初始化成這個類的私有成員"name"的。
class Animal { constructor(private name: string) { } move(meters: number) { alert(this.name + " moved " + meters + "m."); } } var goat = new Animal("Goat"); goat.move(25); // Goat moved 25 m.
通過這種方式使用"private"來創建和初始化私有成員,"public"也一樣。
訪問器
TypeScript提供 getters/setters 的方式來攔截對於對象成員的訪問。它讓我們可以更精確的控制如何對對象成員的進行訪問。
讓我們來將一個類改寫成用"get"和"set"。首先,我們從一個沒有"get"和"set"的例子開始:
class Employee { fullName: string; } var employee = new Employee(); employee.fullName = "Bob Smith"; if (employee.fullName) { alert(employee.fullName); }
以上代碼允許我們隨意設置fullName,可能我們會覺得這樣比較直接和方便,但這麼隨心所欲的改變名字也可能會導致問題。
在這個版本中,我們將給被允許修改員工信息的用戶一個可用的密碼。在對fullName進行"set"訪問的之前,我們會以檢查密碼來代替允許直接修改。我們添加一個相應的"get"讓之前的例子依然能實現。
var 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 { alert("Error: Unauthorized update of employee!"); } } } var employee = new Employee(); employee.fullName = "Bob Smith"; if (employee.fullName) { alert(employee.fullName); }
為了證明現在訪問需要密碼,我們可以修改密碼,然後我們會發現,當密碼不符合的時候會彈出提示"Error: Unauthorized update of employee!"(錯誤:沒有修改employee的許可權)。
註意:訪問器需要我們將文件以ECMAScript5編程輸出。
tsc --target ES5 your.ts
靜態屬性
到此為止,我們值談到類的實例成員,那些只有實例化後才初始化並且顯示的成員。我們還可以為類的創建靜態成員,那些在類本身可見而非實在實例上可見。在這個例子中,我們使用"static"來修飾"origin",因為他是所有Grid都會用到的東西。每個實例想要訪問這個屬性,都需要在前面加上類名。這就像要在實例前面加上"this"來訪問這個實例,這裡我們將使用"Grid."來訪問靜態屬性。
class Grid { static origin = {x: 0, y: 0}; calculateDistanceFromOrigin(point: {x: number; y: number;}) { var xDist = (point.x - Grid.origin.x); var yDist = (point.y - Grid.origin.y); return Math.sqrt(xDist * xDist + yDist * yDist) / this.scale; } constructor (public scale: number) { } } var grid1 = new Grid(1.0); // 1x 規模 var grid2 = new Grid(5.0); // 5x 規模 alert(grid1.calculateDistanceFromOrigin({x: 10, y: 10})); alert(grid2.calculateDistanceFromOrigin({x: 10, y: 10}));
高級技巧
構造函數
當你在TypeScript中聲明一個類的同時,你也定義了很多東西。首先就是這個類的實例類型。
class Greeter { greeting: string; constructor(message: string) { this.greeting = message; } greet() { return "Hello, " + this.greeting; } } var greeter: Greeter; greeter = new Greeter("world"); alert(greeter.greet());
這裡,當我們寫"var greeter: Greeter",我們就已經將"Greeter"類的實例類型定義為"Greeter"了。這對於用過其它面向對象語言的程式員而言已經習以為常了。
我們也同時的創建了一個稱為構造函數的值,當我們使用"new"來為類創建實例的時候,我們將會調用這個函數。讓我們結合實踐,在編譯後的JavaScript中看看上面的這個例子吧:
var Greeter = (function () { function Greeter(message) { this.greeting = message; } Greeter.prototype.greet = function () { return "Hello, " + this.greeting; }; return Greeter; })(); var greeter; greeter = new Greeter("world"); alert(greeter.greet());
在這裡,"var Greeter"是指定構造函數。當我們使用"new"並且執行這個函數之後,便會得到一個類的實例。這個構造函數包含了類的所有的靜態成員。換種說法,即類有靜態部分和實例部分。
讓我們稍微修改下例子看看它們的不同之處:
class Greeter { static standardGreeting = "Hello, there"; greeting: string; greet() { if (this.greeting) { return "Hello, " + this.greeting; } else { return Greeter.standardGreeting; } } } var greeter1: Greeter; greeter1 = new Greeter(); alert(greeter1.greet()); // 上下代碼效果做對比 var greeterMaker: typeof Greeter = Greeter; greeterMaker.standardGreeting = "Hey there!"; var greeter2:Greeter = new greeterMaker(); alert(greeter2.greet());
在這個例子中,"greeter1"和之前例子是一樣的。我們實例化了"Greeter"類,並且使用這個對象。結果也和之前的例子一樣。
接下來,我們直接使用這個類,我們創建了一個名為"greeterMaker"的新變數。這個變數保存了這個類,換種說法即保存了這個構造函數。這裡我們使用"typeof Greeter",這麼做的話"greeterMaker"的類型就成了"Greeter"類的類型,而非"Greeter"的實例的類型("Greeter"類的實例類型為"Greeter")。更準確的說,"給我Greeter類的類型",也就是構造函數的類型。這個類包含"Greeter"類的所有靜態成員和創建"Greeter"類的實例的構造函數。同之前的例子一樣,我們對"greeterMaker"使用"new",用來創建"Greeter"的實例並且觸發。
將類當作介面一樣使用
正如我們在上一節所說的,聲明一個類的同時會創建其他兩個東西:這個類的實例類型和一個構造函數。因為類能夠創建類型,所以在使用interface(介面)的地方都可以使用class(類)。
class Point { x: number; y: number; } interface Point3d extends Point { z: number; } var point3d: Point3d = {x: 1, y: 2, z: 3};