TypeScript 类(Class)

自 ES6 起,终于迎来了 class,对于开发者来说,终于可以使用基于类的面向对象式编程。TypeScript 在原 ES6 中类的基础上,还添加了一些新的功能,比如几种访问修饰符,这是在其他面向对象语言中早就实现了的。

JavaScript 的类作为语法糖,我们不但需要知道怎么去使用,还应该了解其本质,涉及到原型的部分希望大家能深入理解。

1. 慕课解释

类描述了所创建的对象共同的属性和方法。通过 class 关键字声明一个类,主要包含以下模块:

  • 属性
  • 构造函数
  • 方法

2. 类的本质

JavaScript 中,生成实例对象可以通过构造函数的方式:

实例演示
预览 复制
复制成功!
function Calculate (x, y) {
  this.x = x
  this.y = y
}

Calculate.prototype.add = function () {
  return this.x + this.y
}

var calculate = new Calculate(1, 2)
console.log(calculate.add()) // 3
运行案例 点击 "运行案例" 可查看在线运行效果

如果通过 class 关键字进行改写:

实例演示
预览 复制
复制成功!
class Calculate {
  // 类的属性
  public x: number
  public y: number

  // 构造函数
  constructor(x: number, y: number) {
    this.x = x
    this.y = y
  }

  // 类的方法
  add () {
    return this.x + this.y
  }
}

const calculate = new Calculate(1, 2)
console.log(calculate.add()) // 3

console.log(typeof Calculate) // 'function'
console.log(Calculate === Calculate.prototype.constructor) // true
运行案例 点击 "运行案例" 可查看在线运行效果

代码解释:

最后一行,可以看出,类指向其构造函数本身,class 关键字可以看做是一个语法糖。

constructor() 方法是类的默认方法,通过 new 来生成对象实例时,自动调用该方法。换句话说,constructor() 方法默认返回实例对象 this

3. 类的继承

基于类的程序设计中一种最基本的模式是允许使用继承来扩展现有的类,这样可以抽出公共部分让子类复用。

使用 extends 关键字来实现继承:

实例演示
预览 复制
复制成功!
// 继承 JavaScript 内置的 Date 对象
class LinDate extends Date {

  getFormattedDate() {
    var months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
    return this.getDate() + "-" + months[this.getMonth()] + "-" + this.getFullYear();
  }
}

const date = new LinDate()

console.log(date.getFullYear());     // 2020
console.log(date.getFormattedDate()) // 7-Jan-2020
运行案例 点击 "运行案例" 可查看在线运行效果

代码解释: LinDate 继承了 Date 的功能,可以使用父类 Date 的方法 getFullYear(),也可以使用自身的方法 getFormattedDate()

子类在 constructor 内中使用 super() 方法调用父类的构造函数,在一般方法内使用 super.method() 执行父类的方法。

实例演示
预览 复制
复制成功!
class Animal {
  public name:string

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

  move(distance: number = 0) {
      console.log(`${this.name} moved ${distance}m.`)
  }
}

class Dog extends Animal {
  constructor(name: string) { 
    // 调用父类的构造函数
    super(name)
  }

  move(distance = 10) {
      console.log('bark...')
      // 执行父类的方法
      super.move(distance) 
  }
}

const dog: Animal = new Dog('Coco')

dog.move() // Coco moved 10m.
运行案例 点击 "运行案例" 可查看在线运行效果

代码解释:

第 16 行,通过 super() 调用了父类的构造函数。

第 22 行,通过 super 关键字调用父类的方法。

4. 访问修饰符

TypeScript 可以使用四种访问修饰符 public、protected、private 和 readonly。

4.1 public

TypeScript 中,类的成员全部默认为 public,当然你也可以显式的将一个成员标记为 public,标记为 public 后,在程序类的外部可以访问。

class Calculate {
  // 类的属性
  public x: number
  public y: number

  // 构造函数
  public constructor(x: number, y: number) {
    this.x = x
    this.y = y
  }

  public add () {
    return this.x + this.y
  }
}

4.2 protected

当成员被定义为 protected 后,只能被类的内部以及类的子类访问

class Base {
  protected baseUrl: string = 'http://api.com/'

  constructor() {}

  protected request(method: string) {
    const url = `${this.baseUrl}${method}`
    // TODO 封装基础的 http 请求
  }
}

class Address extends Base {
  get() {
    return this.request('address')
  }
}

代码解释:

第 2 行,Base 类的属性 baseUrl 被定义为受保护的,那么第 7 行该属性在类中被访问是可以的。

第 14 行,因 Address 类是 Base 类的子类,在子类中允许访问父类中被定义为受保护类型的方法 request() 。

4.3 private

当类的成员被定义为 private 后,只能被类的内部访问

class Mom {
  private labour() {
    return 'baby is coming'
  }
}

class Son extends Mom {
  test () {
    this.labour() // Error, Property 'labour' is private and only accessible within class 'Mom'
  }
}

代码解释:

第 9 行,父类中的 labour() 方法被定义为私有方法,只能在父类中被使用,子类中调用报错。

4.4 readonly

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

class Token {
  readonly secret: string = 'xjx*xh3GzW#3'

  readonly expired: number

  constructor (expired: number) {
    this.expired = expired
  } 
}

const token = new Token(60 * 60 * 24)
token.expired = 60 * 60 * 2 // Error, expired 是只读的

代码解释:

最后一行,因 Token 类的属性 expired 被设置为只读属性,不可被修改。

5. 静态方法

通过 static 关键字来创建类的静态成员,这些属性存在于类本身上面而不是类的实例上

class User {
  static getInformation () {
    return 'This guy is too lazy to write anything.'
  }
}

User.getInformation() // OK

const user = new User()
user.getInformation() // Error 实例中无此方法

代码解释: getInformation() 方法被定义为静态方法,只存在于类本身上,类的实例无法访问。

静态方法调用同一个类中的其他静态方法,可使用 this 关键字。

class StaticMethodCall {

  static staticMethod() {
      return 'Static method has been called'
  }
  static anotherStaticMethod() {
      return this.staticMethod() + ' from another static method'
  }

}

代码解释: 静态方法中的 this 指向类本身,而静态方法也存在于类本身,所以可以在静态方法中用 this 访问在同一类中的其他静态方法。

非静态方法中,不能直接使用 this 关键字来访问静态方法。而要用类本身或者构造函数的属性来调用该方法:

class StaticMethodCall {
  constructor() {
      // 类本身调用
      console.log(StaticMethodCall.staticMethod())

      // 构造函数的属性调用
      console.log(this.constructor.staticMethod())
  }
  static staticMethod() {
      return 'static method has been called.'
  }
}

代码解释: 类指向其构造函数本身,在非静态方法中,this.constructor === StaticMethodCalltrue, 也就是说这两种写法等价。

6. 抽象类

抽象类作为其它派生类的基类使用,它们一般不会直接被实例化,不同于接口,抽象类可以包含成员的实现细节。

abstract 关键字是用于定义抽象类和在抽象类内部定义抽象方法。

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

const animal = new Animal() // Error, 无法创建抽象类实例

通常我们需要创建子类继承抽象类,将抽象类中的抽象方法一一实现,这样在大型项目中可以很好的约束子类的实现。

class Dog extends Animal {
  makeSound() {
    console.log('bark bark bark...')
  }
}

const dog = new Dog()

dog.makeSound()  // bark bark bark...
dog.move()       // roaming the earch...

7. 把类当做接口使用

类也可以作为接口来使用,这在项目中是很常见的。

class Pizza {
  constructor(public name: string, public toppings: string[]) {}
}

class PizzaMaker {
  // 把 Pizza 类当做接口
  static create(event: Pizza) {
    return new Pizza(event.name, event.toppings)
  }
}

const pizza = PizzaMaker.create({ 
  name: 'Cheese and nut pizza', 
  toppings: ['pasta', 'eggs', 'milk', 'cheese']
})

第 7 行,把 Pizza 类当做接口。

因为接口和类都定义了对象的结构,在某些情况下可以互换使用。如果你需要创建一个可以自定义参数的实例,同时也可以进行类型检查,把类当做接口使用不失为一个很好的方法。

这就是 TypeScript 的强大功能,而且非常灵活,拥有全面的面向对象设计和通用的类型检查

8. 小结

本节介绍了类的本质及其使用方法,需要注意:

  • 类指向其构造函数本身。
  • 静态方法存在于类本身上面而不是类的实例上。
  • 抽象类中的抽象方法不包含具体实现并且必须在派生类中实现。
  • TypeScript 新增了 public、protected、private 等访问修饰符。
  • 子类继承父类时,在其构造函数 constructor() 中不要忘了 super() 方法。