• 继承
    • 私有/公开 描述符
      • 默认是公开的
      • 理解私有
      • 参数属性
    • 访问器
    • 静态属性
    • 高级技巧
      • 构造函数
      • 将类作为接口使用

    传统的JavaScript在构造可复用组件时,使用的是基于原型的继承,这对于习惯了使用基于类的面向对象编程语言的人来说,可能会有些古怪。不过,自从ES6开始,JavaScript也支持书写基于类的对象了。在TypeScript中,我们同样允许开发者书写这种基于类的代码,并且可以编译成跨浏览器,跨平台的通用代码。

    让我们以一个简单的类的例子开始:

    1. class Greeter {
    2. greeting: string;
    3. constructor(message: string) {
    4. this.greeting = message;
    5. }
    6. greet() {
    7. return "Hello, " + this.greeting;
    8. }
    9. }
    10. var greeter = new Greeter("world");

    如果你以前写过C#Java,那么你一定对这种语法十分的熟悉。在这里,我们生成了一个新的Greeter类。这个类有三个成员,一个greeting属性,一个构造函数,以及一个greet方法。

    你可能注意到了,在我们访问内部成员时,我们总是在前面加上this.。这表示我们将访问一个成员变量。

    在最后一行,我们使用new创建了一个Greeter类的实例。这将会调用我们先前声明的类的构造函数。

    继承

    TypeScript使用了最普遍的面向对象模型。所以,它也支持通过继承来扩展类。

    例子:

    1. class Animal {
    2. name:string;
    3. constructor(theName: string) { this.name = theName; }
    4. move(meters: number = 0) {
    5. alert(this.name + " moved " + meters + "m.");
    6. }
    7. }
    8. class Snake extends Animal {
    9. constructor(name: string) { super(name); }
    10. move(meters = 5) {
    11. alert("Slithering...");
    12. super.move(meters);
    13. }
    14. }
    15. class Horse extends Animal {
    16. constructor(name: string) { super(name); }
    17. move(meters = 45) {
    18. alert("Galloping...");
    19. super.move(meters);
    20. }
    21. }
    22. var sam = new Snake("Sammy the Python");
    23. var tom: Animal = new Horse("Tommy the Palomino");
    24. sam.move();
    25. tom.move(34);

    上面的例子中,我们使用了extends关键字,来创建了一个子类。HorseSnake子类都是继承与Animal类的,它们包含Animal类的所有特性。

    子类也可以重载父类的方法,在上面的例子中,SnakeHorse都重新定义了它们各自的move方法。

    私有/公开 描述符

    默认是公开的

    你可能通过上面的例子已经察觉到,我们不必使用public关键字来描述公开的类的属性和方法。而在像C#这样的语言里,你都必须明确地为公开成员添加public标识。不过在TypeScript中,成员默认就是公开的。

    你可能需要将一些成员标识为私有的,来保证它们不会被外部代码所看见。我们来修改一下之前的Animal类:

    1. class Animal {
    2. private name:string;
    3. constructor(theName: string) { this.name = theName; }
    4. move(meters: number) {
    5. alert(this.name + " moved " + meters + "m.");
    6. }
    7. }

    理解私有

    TypeScript有一个结构化的类型系统。当我们比较两个不同的类型时,不论它们来自哪里,如果它们的成员的类型是兼容的,那么我们说这些类型本身是兼容的。

    但是当比较的一方是私有的成员时,我们便会区别对待它们。如果一方是私有成员,那么另一方必须是指向同一个私有成员时,才算兼容。

    例子:

    1. class Animal {
    2. private name:string;
    3. constructor(theName: string) { this.name = theName; }
    4. }
    5. class Rhino extends Animal {
    6. constructor() { super("Rhino"); }
    7. }
    8. class Employee {
    9. private name:string;
    10. constructor(theName: string) { this.name = theName; }
    11. }
    12. var animal = new Animal("Goat");
    13. var rhino = new Rhino();
    14. var employee = new Employee("Bob");
    15. animal = rhino;
    16. animal = employee; //error: Animal and Employee are not compatible

    在上面的例子里,我们有Animal类和Rhino类,并且RhinoAnimal的子类。我们还有另外一个类叫作Employee,它与Animal类的外观类似。我们分别创建了它们的实例,并且将其中一个赋值给另外一个,然后在将employee赋值给animal时,我们就会得到一个报错。因为在AnimalRhino中,它们的私有属性name都是来自于Animal类中的private name: string,所以它们是兼容的。但是,虽然Employee也有私有成员name,但由于它们并不是在同一处定义的,所以它们并不兼容。

    参数属性

    publicprivate关键字还给予了你一个快速初始化成员的方法,通过使用参数属性,你只需一步就可以创建和初始化一个成员。下面的例子是上文例子的改版。注意我们将声明和赋值都放在了private name: string参数上。

    1. class Animal {
    2. constructor(private name: string) { }
    3. move(meters: number) {
    4. alert(this.name + " moved " + meters + "m.");
    5. }
    6. }

    由于使用的是private关键字,所以我们初始化了一个私有成员。当然,使用public时,效果也是类似的。

    访问器

    TypeScript支持getters/setters来作为属性的访问器。

    以下是一个简单的使用getset的例子,但是首先,让我们先从没有它们的代码开始:

    1. class Employee {
    2. fullName: string;
    3. }
    4. var employee = new Employee();
    5. employee.fullName = "Bob Smith";
    6. if (employee.fullName) {
    7. alert(employee.fullName);
    8. }

    以上的例子里,允许用户直接编辑fullName属性是有些危险的。

    在下面的例子中,我们会在用户修改fullName之前,检查一个passcode。同时,我们也为fullName添加一个getter,来使之能完整的运作:

    1. var passcode = "secret passcode";
    2. class Employee {
    3. private _fullName: string;
    4. get fullName(): string {
    5. return this._fullName;
    6. }
    7. set fullName(newName: string) {
    8. if (passcode && passcode == "secret passcode") {
    9. this._fullName = newName;
    10. }
    11. else {
    12. alert("Error: Unauthorized update of employee!");
    13. }
    14. }
    15. }
    16. var employee = new Employee();
    17. employee.fullName = "Bob Smith";
    18. if (employee.fullName) {
    19. alert(employee.fullName);
    20. }

    为了证明我们的访问器是正常运作的,我们可以修改passcode变量,当passcode不匹配时,我们会得到一个警告弹窗。

    注意:使用访问器时,你需要将编译器的输出设置为ECMAScript 5

    静态属性

    目前为止,我们只讨论了类的实例成员,这些成员在类被实例化后,才可见。我们也可以创造类的静态成员,这些成员在类的级别就可见。在下面的例子中,我们使用static关键字,置于origin属性之前,使之成为Grid类级别的静态属性。

    1. class Grid {
    2. static origin = {x: 0, y: 0};
    3. calculateDistanceFromOrigin(point: {x: number; y: number;}) {
    4. var xDist = (point.x - Grid.origin.x);
    5. var yDist = (point.y - Grid.origin.y);
    6. return Math.sqrt(xDist * xDist + yDist * yDist) / this.scale;
    7. }
    8. constructor (public scale: number) { }
    9. }
    10. var grid1 = new Grid(1.0); // 1x scale
    11. var grid2 = new Grid(5.0); // 5x scale
    12. alert(grid1.calculateDistanceFromOrigin({x: 10, y: 10}));
    13. alert(grid2.calculateDistanceFromOrigin({x: 10, y: 10}));

    高级技巧

    构造函数

    当你在TypeScript中创建一个类时,实际上你创建了许多东西。第一个,就是你创建了这个类的实例的类型:

    1. class Greeter {
    2. greeting: string;
    3. constructor(message: string) {
    4. this.greeting = message;
    5. }
    6. greet() {
    7. return "Hello, " + this.greeting;
    8. }
    9. }
    10. var greeter: Greeter;
    11. greeter = new Greeter("world");
    12. alert(greeter.greet());

    上面的例子里,我们使用了Greeter作为其实例的类型,即var greeter: Greeter。当然,这种情况也出现在其他许多面向对象的语言中。

    同时,我们又调用构造函数创建了另一个值。构造函数在我们使用new关键字时会被调用。例子:

    1. var Greeter = (function () {
    2. function Greeter(message) {
    3. this.greeting = message;
    4. }
    5. Greeter.prototype.greet = function () {
    6. return "Hello, " + this.greeting;
    7. };
    8. return Greeter;
    9. })();
    10. var greeter;
    11. greeter = new Greeter("world");
    12. alert(greeter.greet());

    上面的例子中,var Greeter被赋值为了一个构造函数。当我们使用new关键字时,我们创建了这个类的一个实例。这个构造函数也包含了所有类的静态成员。

    让我们稍微修改下这个例子:

    1. class Greeter {
    2. static standardGreeting = "Hello, there";
    3. greeting: string;
    4. greet() {
    5. if (this.greeting) {
    6. return "Hello, " + this.greeting;
    7. }
    8. else {
    9. return Greeter.standardGreeting;
    10. }
    11. }
    12. }
    13. var greeter1: Greeter;
    14. greeter1 = new Greeter();
    15. alert(greeter1.greet());
    16. var greeterMaker: typeof Greeter = Greeter;
    17. greeterMaker.standardGreeting = "Hey there!";
    18. var greeter2:Greeter = new greeterMaker();
    19. alert(greeter2.greet());

    在上面的例子中,greeter1与上面例子中的实例一模一样。我们实例化了Greeter类,然后使用它。

    然后,我们直接使用类。我们创建了一个greeterMaker变量。这个变量的值就是类本身,或者说就是类的构造函数。我们使用typeof Greeter来定义其类型,即它的构造函数的类型。这个类型还会包含类的所有静态成员。然后,我们可以在greeterMaker之前使用new关键字,来创建一个新的实例,并且使用它。

    将类作为接口使用

    正如我们在上一节中所提到的,一个类声明其实创建了两件东西:一个代表类实例的类型,以及一个构造函数。因为类能创建类型,所以你也可以在能使用接口的地方使用它。

    1. class Point {
    2. x: number;
    3. y: number;
    4. }
    5. interface Point3d extends Point {
    6. z: number;
    7. }
    8. var point3d: Point3d = {x: 1, y: 2, z: 3};