类_TypeScript笔记4

一.类成员

TypeScript里的类的定义与ES6 Class规范一致,静态属性,实例属性,访问器等都支持:

class Grid {
    static origin = {x: 0, y: 0};
}

class Employee {
    private _fullName: string;

    get fullName(): string {
        return this._fullName;
    }

    set fullName(newName: string) {
        this._fullName = newName;
    }
}

但需要注意2点:

  • ES3不支持getter/setter,因此要求编译配置为ES5+

  • 只有getter没有setter的属性会被自动推断为readonly(成员修饰符之一)

二.成员修饰符

访问控制修饰符

支持3个访问控制修饰符:

  • public:类的成员属性/方法默认都是public,没有访问限制

  • private:无法在该类声明的外部访问其成员(如无法通过this.xxx访问私有成员)

  • protected:与private类似,但在派生类中也可以访问受保护成员

例如:

class Animal {
    // 私有成员属性
    private name: string;
    // 受保护成员属性
    protected ancestor: string;
    // 不写的话,默认public
    constructor(theName: string) { this.name = theName; }
    // public成员方法
    public move(distanceInMeters: number) {
        console.log(`${this.name} moved ${distanceInMeters}m.`);
    }
}

注意,这些访问控制都只是编译时的限制,运行时并不做强检查。符合TypeScript的设计原则:

不给编译产物增加运行时开销

另外,类成员可访问性也是类型检查的一部分,private/protected修饰符会打破鸭子类型,例如:

class Animal {
    name: string;
    constructor(theName: string) { this.name = theName; }
}
class Person {
    name: string;
    constructor(theName: string) { this.name = theName; }
}
// 正确:鸭子类型(长得像就是)
let person: Animal = new Person('Sam');

class Plant {
    // 私有成员name
    private name: string;
    constructor(theName: string) { this.name = theName; }
}
// 错误:name属性可访问性不匹配(长得像也不行,可访问性不一样)
// Property 'name' is private in type 'Plant' but not in type 'Person'.
let invalidPlant: Plant = new Person('Stone');

P.S.特殊的,protected constructor表示该类不允许直接实例化,但支持继承

readonly修饰符

可以通过readonly修饰符声明属性只读,只能在声明时或构造函数里赋值,例如:

class Octopus {
    readonly name: string;
    public readonly numberOfLegs: number = 8;
    constructor (theName: string) {
        this.name = theName;
    }
}
let dad = new Octopus("Man with the 8 strong legs");
dad.name = "Man with the 3-piece suit"; // error! name is readonly.

P.S.当然,readonly与访问控制修饰符并不冲突,可以作用于同一个属性

参数属性

对于在构造函数里初始化的属性:

class Octopus {
    readonly name: string;
    constructor (theName: string) {
        this.name = theName;
    }
}

有一种简写方式

class Octopus {
    readonly numberOfLegs: number = 8;
    constructor(readonly name: string) {
    }
}

其中,name参数属性,通过给构造函数的形参名前添上private/protected/public/readonly修饰符来声明

三.继承

class A extends B {
    //...
}

类似于Babel转换(清晰起见,仅摘录关键部分):

function _inherits(subClass, superClass) {
  // 继承父类原型属性
  subClass.prototype = Object.create(superClass && superClass.prototype, {
    constructor: {
      value: subClass,
      enumerable: false,
      writable: true,
      configurable: true
    }
  });
  // 继承父类静态属性
  if (superClass)
    Object.setPrototypeOf
      ? Object.setPrototypeOf(subClass, superClass)
      : (subClass.__proto__ = superClass);
}

// 子类构造函数中继承父类实例属性
function A() {
    // 通过父类构造函数给子类实例this添上父类实例属性
    return A.__proto__ || Object.getPrototypeOf(A)).apply(this, arguments)
}

TypeScript里的Class继承也会被编译替换成基于原型的继承,如下:

var __extends = (this && this.__extends) || (function () {
    var extendStatics = function (d, b) {
        extendStatics = Object.setPrototypeOf ||
            ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
            function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
        return extendStatics(d, b);
    };

    return function (d, b) {
        // 继承父类静态属性
        extendStatics(d, b);
        function __() { this.constructor = d; }
        // 继承父类原型属性,及实例属性
        d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
    };
})();

// __extends(A, B);

二者大同小异,从实现上看,TypeScript编译产物更健壮,因为其目标是:

在任何支持 ES3+的宿主环境中运行

P.S.比较有意思的是静态属性的继承,具体见一.如何继承静态属性?

四.抽象类

TypeScript里也有抽象类的概念:

abstract class Animal {
    abstract makeSound(): void;
    move(): void {
        console.log('roaming the earch...');
    }
}

抽象类里可以有带实现的具体方法(如move),也可以有只声明不实现的抽象方法(如makeSound),但要求子类必须实现这些方法:

class Cat extends Animal {
    makeSound() {
        console.log('meow meow meow');
    }
}

另一个相似的概念是接口,二者区别在于接口中只能定义“抽象方法”(没有abstract关键字,确切地说是方法签名),例如:

interface Animal {
  // 对比 abstract makeSound(): void;
  makeSound(): void;
}

// 对比 class Cat extends Animal
class Cat implements Animal {
    makeSound() {
        console.log('meow meow meow');
    }
}

五.类与类型

声明一个类的同时,也声明了该类实例的类型,例如:

class Greeter {
    static standardGreeting = "Hello, there";
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }
    greet() {
        return "Hello, " + this.greeting;
    }
}

let greeter: Greeter;
greeter = new Greeter("world");

其中,实例greeterGreetr类型的,也就是说,Class声明具有类型含义

  • 该类实例的类型:Greeter

  • 类自身的类型:typeof Greeter

实际上,类自身的类型约束了静态属性、实例属性、构造函数、原型方法等特征,例如:

class GreeterDuck {
    // 类自身的类型约束
    static standardGreeting = 'Hi';
    greeting: string;
    constructor(message: string) { }
    greet() { return 'there'; }
    // 允许多不允许少(鸭子类型)
    sayHello() { /*...*/ }
}

let greeterType: typeof Greeter = GreeterDuck;

更进一步的:

// 从Class类型扩展出Interface
interface HelloGreeter extends Greeter {
    sayHello(): void;
}

let hGreeter: HelloGreeter = new GreeterDuck('world');

没错,因为类具有类型含义,所以接口能够继承自类

参考资料

发表评论

电子邮件地址不会被公开。 必填项已用*标注

*

code