首页 > 编程笔记 > JavaScript笔记 阅读:6

TypeScript类的定义和使用(非常全面,附带实例)

对于传统的 JavaScript 程序,开发者可以使用函数和基于原型的继承来创建可重用的组件,但因为熟悉使用面向对象方式的开发者更习惯使用类的相关语法,所以使用这些语法就不会很顺畅。

为了提升 JavaScript 的包容性,从 ES6 开始,允许 JavaScript 开发者使用基于类的面向对象的方式进行开发。

在 TypeScript 中,允许开发者使用面向对象的相关特性,并且被编译后的 JavaScript 可以在所有主流浏览器和平台上运行,而不需要等到下一个 JavaScript 版本。

下面来看一个使用类的代码,读者可先自行进行分析。
/*
类的基本定义与使用
*/
class Person {
  // 声明属性(必须写)
  name: string;
  age: number;

  // 构造函数
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }

  // 一般方法
  sayInfo(): void {
    console.log(`我叫${this.name},今年${this.age}`);
  }
}

// 创建类的实例对象,内部会调用 Person 的构造函数
const p = new Person('tom', 12);
// 调用实例的方法
p.sayInfo();
执行结果为:

我叫tom,今年12

如果读者学习过 C# 或 Java,则会对上面这种语法非常熟悉。在上面的代码中声明了一个 Person 类,该类中有 4 个成员,分别是属性 name、属性 age、构造函数和 sayInfo() 方法。

读者可能会注意到,在方法中引用任何一个类中定义的实例成员时都使用了 this.成员名。这就表示访问的是实例成员。在代码中,我们使用 new 构造了 Person 类的一个实例对象。它会调用之前定义的构造函数,创建一个 Person 类型的新实例对象,并执行构造函数来初始化实例对象的属性。最后通过 p 对象调用其 sayInfo() 方法。

TypeScript类的继承

在 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); // 可以调用从父类中继承到的方法
运行结果为:

wang! wang!
Animal run 100m

上面的代码展示了最基本的继承,类 Dog 从基类 Animal 中继承了属性和方法。其中 Dog 是一个派生类,它通过 extends 关键字派生自 Animal 基类。需要注意的是,派生类通常被称为子类,基类通常被称为父类。

在上面的代码中,因为 Dog 继承了 Animal 的功能,因此可以创建一个 Dog 的实例,该实例能够调用 cry() 和 run() 方法。

TypeScript类的多态

所谓多态,就是由继承产生了相关的不同的类,对同一个方法可以有不同的响应。比如 Snake 类和 Horse 类都继承自 Animal 类,但是分别实现了自己的 eat() 方法。

此时针对某一个实例,开发者无须了解它是 Cat 还是 Dog,就可以直接调用 run() 方法,程序会自动判断应该如何执行 run() 方法。

对于多态简单的理解就是,声明某种类型的对象,但实际指定的可以是“某种类型”的对象,也可以是任意子类型的对象,也就是传入的对象有多种形态。

请读者思考下面代码的运行结果:
/*
父类型
*/
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);
  }
}

// 声明接收父类型对象
// 多态:多种形态 => 声明需要的是一个Animal类型的对象
// 实际可以传入animal/horse/snake,最终调用的都是实际对象的方法
function run(animal: Animal) {
  animal.run();
}

run(new Animal('aa'));
run(new Snake('ss'));
run(new Horse('hh'));
运行结果为:

aa run 0m
sliding...
ss run 5m
dashing...
hh run 50m

TypeScript访问修饰符

访问修饰符一共有 3 种,分别是 public、private 和 protected,下面分别进行介绍。

1) public

当我们在类中定义成员时,如果不使用任何访问修饰符,则默认是 public,当然也可以显式地写上 public。一般称该成员为公开的属性或方法,访问它是不受限制的,在类体内部或类体外部都可以通过实例对象来访问它。

2) private

当成员被标记成 private 时,一般被称为私有的属性或方法,只能在类体内部被访问,出了类体就不能被访问了。

3) protected

protected 修饰符与 private 修饰符的作用相似,但有一点不同,protected 成员在派生的子类中仍旧可以被访问。

下面通过一段代码演示上面 3 种访问修饰符的用法:
/*
访问修饰符:用来描述类内部的属性或方法的可访问性
public:默认值,公开的,类体内部或外部都可以访问
private:只有类体内部才可以访问
protected:类体内部或子类可以访问
*/

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); // 私有的,不可见
运行结果为:

abc

TypeScript readonly修饰符

readonly 修饰符可以将属性设置为只读的。需要注意的是,只读属性必须在声明时或在构造函数中被初始化。

请读者思考下面代码的运行结果:
class Person {
  readonly name: string = 'abc';
  constructor(name: string) {
    this.name = name;
  }
}

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

TypeScript静态属性

其实,TypeScript 还可以创建类的静态属性,这些属性存在于类本身而不是类的实例对象上,只需利用 static关键字定义属性即可。

比如通过 static 关键字定义 Person 类中的 name2 属性,当要访问这个属性时,需要通过 Person.name2 来访问,而没有添加 static 关键字的属性是非静态属性,需要通过类的实例对象来访问。

请读者思考下面代码的运行结果:
/*
静态属性是类本身的属性
非静态属性是类的实例对象的属性
*/

class Person {
  name1: string = 'A';
  static name2: string = 'B';
}

console.log(Person.name2);
console.log(new Person().name1);
运行结果为:

B
A

TypeScript抽象类

有一种类型,它有确定的行为,也有不确定的行为,对于这种场景就可以使用抽象类来实现。

抽象类主要作为其他子类的父类使用,不能被实例化。不同于接口,抽象类可以包含成员的实现细节。可以通过关键字 abstract 定义抽象类和抽象类中的抽象方法。

请读者思考下面代码的运行结果:
abstract class Animal {
  abstract cry();
  run() {
    console.log('run()');
  }
}

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

const dog = new Dog();
dog.cry();
dog.run();
运行结果为:

Dog cry()
run()

相关文章