TypeScript手册翻译系列3-类

笑着哭i 提交于 2019-11-28 12:29:01

传统的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手册翻译系列1-基础类型, http://my.oschina.net/1pei/blog/493181

[5] TypeScript手册翻译系列2-接口, http://my.oschina.net/1pei/blog/493388

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!