Skip to content

类 ​

类的概念 ​

在介绍 TypeScript 中类的用法之前,我们有必要先在这里对类相关的概念做一个简单的介绍。

  • 类(Class):定义了一件事物的抽象特点,包含它的属性和方法;
  • 对象(Object):类的实例,通过 new 生成;
  • 面向对象(OOP)的三大特性:封装、继承、多态;
  • 封装(Encapsulation):将对数据的操作细节隐藏起来,只暴露对外的接口。外界调用端不需要(也不可能)知道细节,就能通过对外提供的接口来访问该对象,同时也保证了外界无法任意更改对象内部的数据;
  • 继承(Inheritance):子类继承父类,子类除了拥有父类的所有特性外,还有一些更具体的特性;
  • 多态(Polymorphism):由继承而产生了相关的不同的类,对同一个方法可以有不同的响应。比如 Cat 和 Dog 都继承自 Animal,但是分别实现了自己的 eat 方法。此时针对某一个实例,我们无需了解它是 Cat 还是 Dog,就可以直接调用 eat 方法,程序会自动判断出来应该如何执行 eat;
  • 存取器(getter & setter):用以改变属性的读取和赋值行为;
  • 修饰符(Modifiers):修饰符是一些关键字,用于限定成员或类型的性质。比如 public 表示公有属性或方法;
  • 抽象类(Abstract Class):抽象类是供其他类继承的基类,抽象类不允许被实例化。抽象类中的抽象方法必须在子类中被实现;
  • 接口(Interfaces):不同类之间公有的属性或方法,可以抽象成一个接口。接口可以被类实现(Implements)。一个类只能继承自另一个类,但是可以实现多个接口

类的基本定义与使用 ​

typescript
class Greeter {
  // 声明属性
  greeting: string
  // 构造方法
  constructor(message: string) {
    this.greeting = message
  }
  // 一般方法
  greet() {
    return 'Hello, ' + this.greeting
  }
}
// 创建类的实例
let greeter = new Greeter('world')
// 调用实例的方法
greeter.greet()

类的继承 ​

typescript
class Animal {
  run(distance: number) {
    console.log(`Animal run ${distance}m`)
  }
}

class Dog extends Animal {
  cry() {
    console.log('wang! wang!')
  }
}

const dog = new Dog()
dog.cry()
dog.run(100) // 可以调用

使用 extends 关键字实现继承,子类中使用 super 关键字来调用父类的构造函数和方法。从基类中继承了属性和方法。 这里,Dog 是一个派生类,它派生自 Animal 基类,通过 extends 关键字。 派生类通常被称作子类,基类通常被称作超类。

因为 Dog 继承了 Animal 的功能,因此我们可以创建一个 Dog 的实例,它拥有 cry() 和 run() 实例方法。

typescript
class Animal {
  name: string

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

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

class Snake extends Animal {
  constructor(name: string) {
    // 调用父类型构造方法
    super(name)
  }

  // 重写父类型的方法
  run(distance: number = 5) {
    console.log('sliding...')
    super.run(distance)
  }
}

class Horse extends Animal {
  constructor(name: string) {
    // 调用父类型构造方法
    super(name)
  }

  // 重写父类型的方法
  run(distance: number = 50) {
    console.log('dashing...')
    // 调用父类型的一般方法
    super.run(distance)
  }

  xxx() {
    console.log('xxx()')
  }
}

const snake = new Snake('sn')
snake.run()

const horse = new Horse('ho')
horse.run()

// 父类型引用指向子类型的实例 ==> 多态
const tom: Animal = new Horse('ho22')
tom.run()

/* 如果子类型没有扩展的方法, 可以让子类型引用指向父类型的实例 */
const tom3: Snake = new Animal('tom3')
tom3.run()
/* 如果子类型有扩展的方法, 不能让子类型引用指向父类型的实例 */
// const tom2: Horse = new Animal('tom2')
// tom2.run()

这个例子展示了一些上面没有提到的特性。 这一次,我们使用 extends 关键字创建了 Animal 的两个子类:Horse 和 Snake。

与前一个例子的不同之处是,派生类拥有自己的构造函数,并且在自己的构造函数中必须调用 super() 来执行基类的构造函数。 而且,在构造函数里访问 this 的属性之前,我们一定要调用 super()。

这个例子演示了如何在子类里可以重写父类的方法。Snake 类和 Horse 类都创建了 run 方法,它们重写了从 Animal 继承来的 run 方法,使得 run 方法根据不同的类而具有不同的功能。注意,即使 tom 被声明为 Animal 类型,但因为它的值仍然是 Horse,所以调用 tom.run(34) 时,它还是会调用 Horse 里重写的方法。

存取器 ​

TypeScript 支持通过 getter/setter 来改变属性的赋值和读取行为。 它能帮助你有效的控制对对象成员的访问。

typescript
class Person {
  firstName: string = 'A'
  lastName: string = 'B'
  get fullName() {
    return this.firstName + '-' + this.lastName
  }
  set fullName(value) {
    const names = value.split('-')
    this.firstName = names[0]
    this.lastName = names[1]
  }
}

const p = new Person()
console.log(p.fullName)

p.firstName = 'C'
p.lastName = 'D'
console.log(p.fullName)

p.fullName = 'E-F'
console.log(p.firstName, p.lastName)

类的静态成员 ​

到目前为止,我们只讨论了类的实例成员,那些仅当类被实例化的时候才会被初始化的属性。 我们也可以创建类的静态成员,这些属性存在于类本身上面而不是类的实例上。

静态属性, 是类对象的属性,非静态属性, 是类的实例对象的属性。

静态成员分为静态属性和静态方法,在 ES7 提案中,我们可以在变量或方法的前面加上 static 关键字来定义静态属性或静态方法。

  • 静态属性

    typescript
    class Animal {
      static num = 42
    
      constructor() {
        // ...
      }
    }
    
    console.log(Animal.num) // 42
  • 静态方法

    typescript
    class Animal {
      static isAnimal(a) {
        return a instanceof Animal
      }
    }
    
    let a = new Animal('Jack')
    Animal.isAnimal(a) // true
    a.isAnimal(a) // TypeError: a.isAnimal is not a function

访问修饰符 ​

TypeScript 可以使用三种访问修饰符(Access Modifiers),分别是 public、private 和 protected。

  • public 修饰的属性或方法是公有的,可以在任何地方被访问到,默认所有的属性和方法都是 public 的;
  • private 修饰的属性或方法是私有的,不能在声明它的类的外部访问;
  • protected 修饰的属性或方法是受保护的,它和 private 类似,区别是它在子类中也是允许被访问的;

public ​

在上面的例子里,我们可以自由的访问程序里定义的成员。 如果你对其它语言中的类比较了解,就会注意到我们在之前的代码里并没有使用 public 来做修饰;例如,C# 要求必须明确地使用 public 指定成员是可见的。 在 TypeScript 里,成员都默认为 public。

ts
class Animal {
  public name
  public constructor(name) {
    this.name = name
  }
}

let a = new Animal('Jack')
console.log(a.name) // Jack
a.name = 'Tom'
console.log(a.name) // Tom

上面的例子中,name 被设置为了 public,所以直接访问实例的 name 属性是允许的。

private ​

很多时候,我们希望有的属性是无法直接存取的,这时候就可以用 private 了,当成员被标记成 private 时,它就不能在声明它的类的外部访问。

ts
class Animal {
  private name
  public constructor(name) {
    this.name = name
  }
}

let a = new Animal('Jack')
console.log(a.name) // Jack
a.name = 'Tom'

// index.ts(9,13): error TS2341: Property 'name' is private and only accessible within class 'Animal'.
// index.ts(10,1): error TS2341: Property 'name' is private and only accessible within class 'Animal'.

需要注意的是,TypeScript 编译之后的代码中,并没有限制 private 属性在外部的可访问性。

上面的例子编译后的代码是:

js
var Animal = (function () {
  function Animal(name) {
    this.name = name
  }
  return Animal
})()
var a = new Animal('Jack')
console.log(a.name)
a.name = 'Tom'

使用 private 修饰的属性或方法,在子类中也是不允许访问的:

ts
class Animal {
  private name
  public constructor(name) {
    this.name = name
  }
}

class Cat extends Animal {
  constructor(name) {
    super(name)
    console.log(this.name)
  }
}

// index.ts(11,17): error TS2341: Property 'name' is private and only accessible within class 'Animal'.

而如果是用 protected 修饰,则允许在子类中访问。

protected ​

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

typescript
class Animal {
  public name: string

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

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

class Person extends Animal {
  private age: number = 18
  protected sex: string = '男'

  run(distance: number = 5) {
    console.log('Person jumping...')
    super.run(distance)
  }
}

class Student extends Person {
  run(distance: number = 6) {
    console.log('Student jumping...')

    console.log(this.sex) // 子类能看到父类中受保护的成员
    // console.log(this.age) //  子类看不到父类中私有的成员

    super.run(distance)
  }
}

console.log(new Person('abc').name) // 公开的可见
// console.log(new Person('abc').sex) // 受保护的不可见
// console.log(new Person('abc').age) //  私有的不可见

readonly 修饰符 ​

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

typescript
class Person {
  readonly name: string = 'abc'
  constructor(name: string) {
    this.name = name
  }
}

let john = new Person('John')
// john.name = 'peter' // error

参数属性 ​

在上面的例子中,我们必须在 Person 类里定义一个只读成员 name 和一个参数为 name 的构造函数,并且立刻将 name 的值赋给 this.name,这种情况经常会遇到。 参数属性可以方便地让我们在一个地方定义并初始化一个成员。 下面的例子是对之前 Person 类的修改版,使用了参数属性:

typescript
class Person2 {
  constructor(readonly name: string) {}
}

const p = new Person2('jack')
console.log(p.name)

注意看我们是如何舍弃参数 name,仅在构造函数里使用 readonly name: string 参数来创建和初始化 name 成员。 我们把声明和赋值合并至一处。

参数属性通过给构造函数参数前面添加一个访问限定符来声明。使用 private 限定一个参数属性会声明并初始化一个私有成员;对于 public 和 protected 来说也是一样

抽象类 ​

抽象类做为其它派生类的基类使用。abstract 关键字用于定义抽象类和在抽象类内部定义抽象方法。

什么是抽象类?

首先,抽象类是不允许被实例化的:

ts
abstract class Animal {
  public name
  public constructor(name) {
    this.name = name
  }
  public abstract sayHi()
}

let a = new Animal('Jack')

// index.ts(9,11): error TS2511: Cannot create an instance of the abstract class 'Animal'.

上面的例子中,我们定义了一个抽象类 Animal,并且定义了一个抽象方法 sayHi。在实例化抽象类的时候报错了。

其次,抽象类中的抽象方法必须被子类实现:

ts
abstract class Animal {
  public name
  public constructor(name) {
    this.name = name
  }
  public abstract sayHi()
}

class Cat extends Animal {
  public eat() {
    console.log(`${this.name} is eating.`)
  }
}

let cat = new Cat('Tom')

// index.ts(9,7): error TS2515: Non-abstract class 'Cat' does not implement inherited abstract member 'sayHi' from class 'Animal'.

上面的例子中,我们定义了一个类 Cat 继承了抽象类 Animal,但是没有实现抽象方法 sayHi,所以编译报错了。

下面是一个正确使用抽象类的例子:

ts
abstract class Animal {
  public name
  public constructor(name) {
    this.name = name
  }
  public abstract sayHi()
}

class Cat extends Animal {
  public sayHi() {
    console.log(`Meow, My name is ${this.name}`)
  }
}

let cat = new Cat('Tom')

上面的例子中,我们实现了抽象方法 sayHi,编译通过了。

如有转载或 CV 的请标注本站原文地址