传统的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};