TypeScript系列 --- 类

139 阅读3分钟

这是我参与8月更文挑战的第6天,活动详情查看:8月更文挑战

传统的JavaScript程序使用函数和基于原型的继承来创建可重用的组件,但对于熟悉使用面向对象方式的程序员来讲就有些棘手,因为他们用的是基于类的继承并且对象是由类构建出来的。 从ECMAScript 2015,也就是ECMAScript 6开始,JavaScript程序员将能够使用基于类的面向对象的方式。

class Greeter {
  greeting: string; // 叫做greeting的属性
  constructor(message: string) { // 构造函数
    this.greeting = message;
  }
  greet() { // greet方法
    return "Hello, " + this.greeting;
  }
}

let greeter = new Greeter("world"); // 实例greeter的类型就是Greeter

继承

基于类的程序设计中一种最基本的模式是允许使用继承来扩展现有的类

class Animal {
  name: string;

  constructor(name: string) { this.name = name; }

  move(distanceInMeters: number = 0) {
      console.log(`Animal moved ${distanceInMeters}m.`);
  }
}

class Dog extends Animal {
  constructor(name: string) {
    // 在构造函数里访问this的属性之前,必须调用super方法以进行初始化操作
    super(name);
  }

  // 重新父类方法
  move(distanceInMeters: number = 0) {
    console.log(`${this.name} moved ${distanceInMeters}m.`);
}

  bark() {
      console.log('Woof! Woof!');
  }
}

const dog = new Dog('Steven');
dog.bark(); // succes
dog.move(10); // success

const dog1: Animal = new Dog('Jhon');
dog1.bark(); // error 此时dog1的类型是Animal
// 实际执行结果为 Jhon moved 5m.
// ts只是对类型进行约束,所以在实际运行的时候,dog1的依旧是Dog的实例对象
dog1.move(5); // success 

在这里,Dog是一个_派生类_,它派生自Animal基类,通过extends关键字。 派生类通常被称作_子类_,基类通常被称作_超类_或父类。

成员修饰符

public

public是默认值

class Animal {
  name: string;
  constructor(theName: string) { this.name = theName; }
  move(distanceInMeters: number) {
      console.log(`${this.name} moved ${distanceInMeters}m.`);
  }
}

这么写和下面这种写法的作用是一致的

class Animal {
    public name: string;
    public constructor(theName: string) { this.name = theName; }
    public move(distanceInMeters: number) {
        console.log(`${this.name} moved ${distanceInMeters}m.`);
    }
}

private

当成员被标记成private时,它就不能在声明它的类的外部访问

class Animal {
    private name: string;
    constructor(name: string) { this.name = name; }
}

new Animal("Cat").name; // error: 'name' 是私有的.

TypeScript使用的是结构性类型系统。当我们比较两种不同的类型时,并不在乎它们从何处而来,如果所有成员的类型都是兼容的,我们就认为它们的类型是兼容的。

class Animal {
  name: string
  constructor(name: string) { this.name = name }
}

class Employee {
  name: string
  constructor(name: string) { this.name = name }
}

let animal = new Animal('Goat');
let employee = new Employee('Klaus');

animal = employee;

虽然animalemployee的类型是不一致的,但是因为ypeScript使用的是结构性类型系统

animalemployee中都存在name属性,所以typescript认为aniamlemployee的类型是兼容的

可以相互赋值。然而,当我们比较带有privateprotected成员的类型的时候,情况就不同了。

class Animal {
  private name: string;
  constructor(name: string) { this.name = name; }
}

class Rhino extends Animal {
  constructor() { super("Rhino"); }
}

class Employee {
  private name: string;
  constructor(name: string) { this.name = name; }
}

let animal = new Animal("Goat");
let rhino = new Rhino();
let employee = new Employee("Bob");

animal = rhino; // success ==> rhino是animal的子类,所以也有私有属性name
// error ===> 虽然animal和employee都有私有属性name
// 但是因为name是私有的,所以相互是不可见的
animal = employee; 

protected

protected修饰符与private修饰符的行为很相似,但有一点不同,protected成员在派生类中仍然可以访问

class Person {
    protected name: string;
    constructor(name: string) { this.name = name; }
}

class Employee extends Person {
    private department: string;

    constructor(name: string, department: string) {
        super(name)
        this.department = department;
    }

    public getElevatorPitch() {
        return `Hello, my name is ${this.name} and I work in ${this.department}.`;
    }
}

let howard = new Employee("Howard", "Sales");

// 我们不能在Person类外使用name,但是我们仍然可以通过Employee类的实例方法访问,因为Employee是由Person派生而来的。
console.log(howard.getElevatorPitch());
console.log(howard.name); // error

构造函数也可以被标记成protected。 这意味着这个类不能在包含它的类外被实例化,但是能被继承。比如,

class Person {
  protected name: string;
  protected constructor(name: string) { this.name = name; }
}

// Employee 能够继承 Person
class Employee extends Person {
  private department: string;

  constructor(name: string, department: string) {
      super(name);
      this.department = department;
  }

  public getElevatorPitch() {
      return `Hello, my name is ${this.name} and I work in ${this.department}.`;
  }
}

let howard = new Employee("Howard", "Sales");
let john = new Person("John"); // 错误: 'Person' 的构造函数是被保护的.

使用子类构造函数初始化父类属性的时候,子类的修饰符的限制作用应小于父类中对应属性的修饰符

class Person {
  protected name: string
  constructor(name: string) { this.name = name }
}

class Student extends Person {
  private name: string = ''
  constructor(name: string) {
    super(name) // error 
   }
}
class Person {
  protected name: string
  constructor(name: string) { this.name = name }
}

class Student extends Person {
  name: string = ''
  constructor(name: string) {
    super(name) // success 
   }
}

readonly

可以使用readonly关键字将属性设置为只读的。 只读属性必须在声明时或构造函数里被初始化。

class Octopus {
  readonly name: string;
  readonly numberOfLegs: number = 8;
  constructor (name: string) {
      this.name = name;
  }
}
let dad = new Octopus("Man with the 8 strong legs");
dad.name = "Man with the 3-piece suit"; // error => name 是只读的.

参数属性

在上面的例子中,我们不得不在在Person类里定义一个只读成员name和一个构造函数参数name。这样做是为了在Octopus构造函数被执行后,就可以访问name的值。

这种情况经常会遇到。typeScript为这种情况定义了一种语法糖_参数属性_,

_参数属性_可以方便地让我们在一个地方定义并初始化一个成员。

class Animal {
  	/**
  	*  constructor (name: string) {
  	*    this.name = name;
  	*  }
  	*/
  	// 上面的构造函数可以被简写为如下形式
  	// 参数属性通过给构造函数参数添加一个访问限定符(public, protected,private)来声明
    constructor(private name: string) { }
    move(distanceInMeters: number) {
        console.log(`${this.name} moved ${distanceInMeters}m.`);
    }
}

存取器

TypeScript支持通过getters/setters来截取对对象成员的访问。 它能帮助你有效的控制对对象成员的访问

如果一个属性,只带有get不带有set的存取器,那么这个属性会自动被推断为readonly

const fullNameMaxLength = 10;

class Employee {
  	// 私有属性 一般用下划线开头
    private _fullName: string = '';

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

    set fullName(newName: string) {
        if (newName && newName.length > fullNameMaxLength) {
            throw new Error("fullName has a max length of " + fullNameMaxLength);
        }

        this._fullName = newName;
    }
}

let employee = new Employee();

// 如果设置了set方法,在设置值的时候,会自动调用set方法
employee.fullName = "Bob Smith";

if (employee.fullName) {
  	// 如果设置了get方法,在取值的时候,会自动调用get方法
    alert(employee.fullName);
}

静态属性

类可以被分为两个部分: 被类的实例对象使用的实例部分和被类本身使用的静态部分

实例部分是那些仅当类被实例化的时候才会被初始化的属性和方法

静态部分是那些存在于类本身上面而不是类的实例上的属性和方法

在typeScript中,我们可以使用static关键字来定义静态属性和方法

class Animal {
  name: string
  static breed: string
  constructor(name: string) { this.name = name }

  eat() { console.log('eatting...') }
  static play() { console.log('playing...') }
}

const animal = new Animal('Cat')

// 访问实例方法和实例属性
console.log(animal.name)
animal.eat()

// 访问静态方法和静态属性
console.log(Animal.breed)
Animal.play()

抽象类

  1. 抽象类做为其它派生类的基类使用
  2. 抽象类不能被直接实例化, 因为抽象类中的抽象方法不包含具体实现并且必须在派生类中实现
  3. 不同于接口,抽象类可以包含成员的实现细节(抽象类中除抽象函数之外,其他函数可以包含具体实现)
  4. abstract关键字是用于定义抽象类和在抽象类内部定义抽象方法
  5. 抽象方法可以包含访问修饰符
  6. absrtact可以使用在属性上,但是这么做的意义并不大
abstract class Animal {
    abstract makeSound(): void; // 定义抽象方法
  
    move(): void {
        console.log("roaming the earth...");
    }
}

类当做接口使用

类可以创建出类型,所以你能够在允许使用接口的地方使用类。

class Point {
    x: number;
    y: number;
}

interface Point3d extends Point {
    z: number;
}

let point3d: Point3d = {x: 1, y: 2, z: 3};