类
传统的JavaScript语言基于函数和原型链继承机制的方式构建可重用的组件,但这对于OO编程人员来说显得比较笨拙,因为是在类的基础上来继承。从JavaScript标准ECMAScript 6开始可以采用面向对象基于类来构建应用。在TypeScript中开发人员现在就可以使用这些技术,TypeScript可以将它们编译为目前大多数浏览器和平台能允许的普通Javascript代码,可以不用等待下一版本的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‘属性,一个构造函数,和一个‘greet‘方法。
你也许已经注意到了例子中在引用成员时前面的‘this.‘,表示这是一个成员访问。
在最后一行我们利用‘new’关键字创建了一个‘Greeter‘类的实例,这会用‘Greeter‘ shape新建一个对象,并调用我们先前定义的构造函数来初始化此对象。
继承
在TypeScript中我们可以使用我们常用的OO设计模式。当然在基于类的编程中,最基本的一个模式是可以通过继承来扩展存在的类,创建出新的类。可看下面的例子:
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‘, 可以访问‘Animal‘的特性。
这个例子也展示了子类中的方法可重载(override)基类中的方法,’Snake’和’Horse‘子类都各自创建了一个‘move’方法来重载基类’Animal’的‘move’方法,这样每个子类就可以实现特定的功能。
Private/Public修饰符
缺省为Public
你可能注意到了在上例中我们并没有用‘public‘去描述类的每一个成员使其可见。在类似于C#语言中,必须显式地标注‘public‘关键字才能使得类的成员可见。但是在TypeScript中。每个成员缺省就是public。
有时我们希望控制类成员不能被外部看到,就可以将这些成员标记为private。下面代码中我们希望隐藏上一章节中‘Animal‘类的name属性:
class Animal { private name:string; constructor(theName: string) { this.name = theName; } move(meters: number) { alert(this.name + " moved " + meters + "m."); } }
理解私有(private)
TypeScript是一个结构化的类型系统。当比较两个不同类型,不关心它们来自哪里,如果每个成员的类型都是兼容的,那么就认为这两个类型也是兼容的。
当比较有‘private‘成员的类型时,就需要另外处理。当比较两个类型时,如果一个类型拥有私有成员,那么另外一个类型必须包含源于同一个声明的私有成员,才认为这两个类型是兼容的。
可参见下面例子来说明:
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; //error: Animal and Employee are not compatible
上面的例子中有‘Animal‘和‘Rhino‘两个类,‘Rhino‘是‘Animal‘的一个子类。同时我们也定义了一个 ‘Employee‘类,它和‘Animal‘类从形状(shape)上看完全相同。我们创建了这三个类的实例,并相互赋值看看会发生什么。因为‘Animal‘和‘Rhino‘共享‘Animal‘中相同的私有访问声明‘private name: string‘,因此它们是兼容的。但是当我们将‘Employee‘赋值给‘Animal‘时,得到类型不兼容错误。虽然‘Employee‘也有一个私有成员‘name‘,但它与 ‘Animal‘中的私有成员‘name‘是不相同的,因此它们是不兼容的类型。
参数属性(Parameter properties)
可以通过关键字public’和’private创建快捷参数属性方式,来创建并初始化类成员字段。参数属性可以让我们仅用一步就可以创建和初始化类成员。下例是上例中我们去掉了‘theName’,在构造函数中使用‘private name: string’参数,来创建‘name‘成员的同时初始化这个字段。
class Animal { constructor(private name: string) { } move(meters: number) { alert(this.name + " moved " + meters + "m."); } }
这里利用‘private‘为参数属性类创建了一个私有成员并初始化其值,对于public也类似。
访问器(Accessors)
TypeScript支持利用getters/setters来控制对成员的访问,这样可以更细粒度来控制类的成员访问方式。
下面将一个类转化为使用‘get‘和‘set‘方式。先从没有getters/setters的例子开始:
class Employee { fullName: string; } var employee = new Employee(); employee.fullName = "Bob Smith"; if (employee.fullName) { alert(employee.fullName); }
虽然直接设置‘fullName‘成员很方便,但如果有人随意改变人名可能会造成麻烦。
在下边,我们希望将其转化为必须提供一个secret passcode,才能修改employee。通过‘set‘关键字来代替直接访问fullName成员,相应地增加一个‘get‘关键字来访问fullName成员:
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); }
为了证明现在的访问器(Accessors)验证了passcode值,可以尝试修改passcode的值,使其不匹配,就会得到没有权限更新employee的告警信息。
注意:访问器需要设置编译输出为ECMAScript 5。
静态属性(Static Properties)
到这里,我们只是讨论了实例化类的成员,当实例化时就可以通过对象来访问成员。我们也可以创建类的静态成员,是通过类来访问而不是通过实例化对象来访问。在下面这个例子中,我们对原点(‘origin‘)成员使用’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 scale var grid2 = new Grid(5.0); // 5x scale 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‘,这个变量保持了Greeter类的类型信息,换句话说是类的构造函数。这里我们使用‘typeof Greeter‘,它给出Greeter类的类型,而不是实例类型。或者更准确地说,给出符号Greeter的类型就是构造函数的类型。这个类型包含Greeter所有的静态成员,以及创建Greeter类实例的构造函数。我们可以用‘new greeterMaker‘来创建‘Greeter‘的实例,然后调用其方法。
将类用作接口
如上所述,类声明创建了两个东西:一个是类实例的类型,一个是构造函数。因为类创建了类型,所以我们可以将类用在使用接口的地方。
class Point { x: number; y: number; } interface Point3d extends Point { z: number; } var point3d: Point3d = {x: 1, y: 2, z: 3};
参考资料
[1] http://www.typescriptlang.org/Handbook#classes
[2] TypeScript - Classes, 破狼blog, http://greengerong.com/blog/2014/11/17/typescript-classes/
[3] TypeScript系列1-简介及版本新特性, http://my.oschina.net/1pei/blog/493012
[4] TypeScript系列2-手册-基础类型, http://my.oschina.net/1pei/blog/493181
[5] TypeScript系列3-手册-接口, http://my.oschina.net/1pei/blog/493388