切换语言为:繁体

详解 TypeScript 中的类

  • 爱糖宝
  • 2024-10-09
  • 2035
  • 0
  • 0

在 TypeScript (TS) 中,类是面向对象编程的核心概念之一。类是用来创建对象的模板,它封装了对象的状态(属性)和行为(方法)。TypeScript 提供了对类的全面支持,并且还增加了类型检查功能,使得代码更加严谨和易于维护。

类的本质

类是面向对象编程(OOP)中的核心概念,它提供了创建对象的模板。在 TypeScript 中,类是用来封装数据(属性)和操作这些数据的方法的。

class Person {
  name: string; // 定义属性
  age: number;

  // 构造函数,用于初始化类的属性
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }

  // 定义方法
  greet() {
    console.log(`你好,我的名字是 ${this.name},我今年 ${this.age} 岁了。`);
  }
}

// 实例化类
const person1 = new Person("Alice", 30);
person1.greet(); // 输出: 你好,我的名字是 Alice,我今年 30 岁了。

在上面的代码中有如下解释:

  1. 属性:name 和 age 是类的属性,它们存储了每个实例的状态(信息)。

  2. 构造函数:constructor 是类的特殊方法,用于在创建对象时初始化属性。

  3. 方法:greet() 是类的方法,用于表示对象的行为。

最终结果如下图所示:

详解 TypeScript 中的类

静态成员与非静态成员的区别

在 TypeScript 中,类的成员可以分为静态成员和非静态成员。它们的主要区别在于:

  1. 非静态成员:属于每个实例对象。每创建一个新的对象,都会创建一组独立的非静态成员。

  2. 静态成员:属于类本身,而不是实例。静态成员可以通过类名直接访问,不需要实例化对象。

如下代码所示:

class Car {
  static totalCars = 0; // 静态变量,属于类本身
  mileage: number; // 非静态变量,属于每个对象

  constructor(mileage: number) {
    this.mileage = mileage;
    Car.totalCars++; // 更新静态变量
  }

  // 静态方法
  static showTotalCars() {
    console.log(`总共有 ${Car.totalCars} 辆车。`);
  }

  // 非静态方法
  showMileage() {
    console.log(`这辆车的里程是 ${this.mileage} 公里。`);
  }
}

const car1 = new Car(10000);
const car2 = new Car(20000);

Car.showTotalCars(); // 输出: 总共有 2 辆车。
car1.showMileage(); // 输出: 这辆车的里程是 10000 公里。

在上面的代码中有如下解释:

  1. 静态变量 totalCars 是一个类级别的属性,所有 Car 对象共享这个值。

  2. 非静态变量 mileage 是每个 Car 对象独立的属性,创建一个新对象就会有一个独立的 mileage 值。

  3. 静态方法 showTotalCars 是通过类名调用的,而不是通过对象调用的。

最终输出结果如下图所示:

详解 TypeScript 中的类

为什么静态成员不能访问非静态成员?

由于静态成员属于类本身,而非静态成员属于实例。静态成员和非静态成员的生命周期不同。静态成员在类加载时已经存在,而非静态成员依赖于具体的实例对象。

class Example {
  static staticMember = "静态成员";
  nonStaticMember = "非静态成员";

  static staticMethod() {
    console.log(this.staticMember); // 这是正确的,因为 staticMember 是静态的
    // console.log(this.nonStaticMember);  // 错误!静态方法无法访问非静态成员
  }
}

Example.staticMethod(); // 输出: 静态成员

静态方法和属性只与类关联,而非静态属性和方法与对象实例关联。静态方法中没有 this 指向具体的实例,因此无法访问属于某个对象的非静态属性或方法。通过类直接调用静态方法时,类内部没有任何关于实例的上下文。

如何在静态方法中访问非静态成员?

如果要在静态方法中访问非静态成员,可以通过将实例对象作为参数传递给静态方法,进而访问非静态成员。

class Car {
  mileage: number; // 非静态成员
  static totalCars = 0; // 静态成员

  constructor(mileage: number) {
    this.mileage = mileage;
    Car.totalCars++; // 统计创建的车辆数
  }

  static showCarMileage(car: Car) {
    console.log(`这辆车的里程是 ${car.mileage} 公里。`); // 通过传递的实例对象访问非静态成员
  }
}

const car1 = new Car(15000);
Car.showCarMileage(car1); // 输出: 这辆车的里程是 15000 公里。

访问修饰符(public, private, protected)

TypeScript 中的访问修饰符用于控制类成员的访问权限:

  1. public:公有成员,可以在类的外部访问(默认修饰符)。

  2. private:私有成员,只能在类的内部访问,不能在外部或者子类中访问。

  3. protected:受保护的成员,可以在类的内部和子类中访问,但不能在类的外部访问。

class Person {
  public name: string; // 公有属性
  private age: number; // 私有属性
  protected id: number; // 受保护属性

  constructor(name: string, age: number, id: number) {
    this.name = name;
    this.age = age;
    this.id = id;
  }

  public greet() {
    console.log(`你好,我的名字是 ${this.name}`);
  }

  private showAge() {
    console.log(`我的年龄是 ${this.age}`);
  }

  protected showId() {
    console.log(`我的 ID 是 ${this.id}`);
  }
}

const person1 = new Person("Alice", 30, 123);
// person1.age;  // 错误,age 是私有属性,无法在外部访问
person1.greet(); // 输出: 你好,我的名字是 Alice

访问修饰符用于控制类的外部对类内部实现的访问,从而实现信息隐藏,这有助于保护类的内部状态免于随意修改。继承中,protected 成员可以在子类中访问,从而在继承链中共享部分逻辑,而 private 成员则完全封闭。

继承与方法重写

TypeScript 支持类的继承,通过 extends 关键字可以继承父类的属性和方法。继承允许子类扩展父类的功能,并且可以重写父类的方法。

class Animal {
  name: string;

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

  public makeSound(): void {
    console.log(`${this.name} makes a sound.`);
  }
}

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

  // 重写父类的方法
  public makeSound(): void {
    console.log(`${this.name} barks.`);
  }
}

const dog = new Dog("Buddy");
dog.makeSound(); // 输出: Buddy barks.

在子类的构造函数中,super 用于调用父类的构造函数,从而初始化父类的属性。子类还可以重写父类的方法来改变行为。

抽象类和抽象方法

抽象类是不能被实例化的类,它们通常用作基类,定义子类必须实现的抽象方法。抽象方法在抽象类中没有具体实现,必须由子类提供具体实现。

abstract class Animal {
  abstract makeSound(): void; // 抽象方法

  public move(): void {
    console.log("The animal moves.");
  }
}

class Dog extends Animal {
  public makeSound(): void {
    console.log("Woof! Woof!");
  }
}

const dog = new Dog();
dog.makeSound(); // 输出: Woof! Woof!
dog.move(); // 输出: The animal moves.

Animal 是抽象类,不能直接实例化。抽象类用于提供通用功能,并为子类定义行为规范。makeSound 是抽象方法,没有实现,子类必须提供具体实现。

接口(Interfaces)

接口定义了一组类必须实现的规范。接口只定义方法的签名,而不提供方法的具体实现。

class Person {
  private _name: string;

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

  get name(): string {
    return this._name;
  }

  set name(newName: string) {
    if (newName.length > 0) {
      this._name = newName;
    } else {
      console.log("Name cannot be empty.");
    }
  }
}

const person = new Person("Alice");
console.log(person.name); // 获取 name,输出: Alice
person.name = "Bob"; // 设置 name
console.log(person.name); // 输出: Bob

Drivable 是接口,定义了 drive 方法的签名。Car 类通过 implements 实现了 Drivable 接口,并提供了 drive 方法的具体实现。

抽象类 vs 接口

抽象类和接口的基本概念:

  1. 抽象类:

    • 抽象类是不能被直接实例化的类,只能作为基类被继承。

    • 抽象类可以包含抽象方法和具体方法。抽象方法没有实现,必须由子类实现;具体方法可以在抽象类中有实现,子类可以继承这些方法。

    • 抽象类用于定义通用的行为规范,同时可以提供部分实现,适合用于构建类的层次结构。

  2. 接口

    • 接口是用来定义类的行为规范的,它只包含方法和属性的签名,不包含具体实现。

    • 类可以通过 implements 关键字实现接口,必须提供接口中定义的所有方法和属性的实现。

    • 接口主要用于类型检查,并且允许一个类实现多个接口,实现类似于多继承的效果。

抽象类可以包含构造函数、字段(属性)、具体方法、抽象方法,并且抽象方法必须由子类实现,具体方法可以由子类继承和使用。

abstract class Animal {
  protected name: string;

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

  // 抽象方法,没有实现
  abstract makeSound(): void;

  // 具体方法,有实现
  public move(): void {
    console.log(`${this.name} 正在移动。`);
  }
}

class Dog extends Animal {
  constructor(name: string) {
    super(name);
  }

  public makeSound(): void {
    console.log(`${this.name} 汪汪叫!`);
  }
}

const dog = new Dog("小黑");
dog.makeSound(); // 输出: 小黑 汪汪叫!
dog.move(); // 输出: 小黑 正在移动。

接口只能定义方法和属性的签名,不包含任何实现,而且一个类可以实现多个接口,每个接口可以定义类的不同方面的行为。

interface Drivable {
  drive(): void;
}

interface Flyable {
  fly(): void;
}

class Plane implements Drivable, Flyable {
  public drive(): void {
    console.log("飞机在滑行。");
  }

  public fly(): void {
    console.log("飞机在飞行。");
  }
}

const plane = new Plane();
plane.drive(); // 输出: 飞机在滑行。
plane.fly(); // 输出: 飞机在飞行。

抽象类可以包含访问修饰符(public、protected、private),用于控制成员的可见性和访问权限。而接口中的成员默认都是 public,不能使用 private 或 protected。

// 抽象类示例
abstract class Animal {
  protected name: string;

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

  abstract makeSound(): void;

  protected sleep(): void {
    console.log(`${this.name} 正在睡觉。`);
  }
}

class Cat extends Animal {
  constructor(name: string) {
    super(name);
  }

  public makeSound(): void {
    console.log(`${this.name} 喵喵叫!`);
  }

  public rest(): void {
    this.sleep(); // 可以访问受保护的成员
  }
}

const cat = new Cat("小花");
cat.makeSound(); // 输出: 小花 喵喵叫!
cat.rest(); // 输出: 小花 正在睡觉。

// 接口示例
interface Swimmable {
  swim(): void;
}

// 不能在接口中使用访问修饰符
interface Flyable {
  fly(): void;
}

class Fish implements Swimmable {
  public swim(): void {
    console.log("鱼在游泳。");
  }
}

抽象类可以包含字段(属性)定义,并且可以有默认值。而接口不能包含字段,只能定义方法签名和属性类型。

// 抽象类可以包含属性
abstract class Vehicle {
  protected speed: number = 0;

  constructor(speed: number) {
    this.speed = speed;
  }

  abstract accelerate(amount: number): void;
}

class Car extends Vehicle {
  public accelerate(amount: number): void {
    this.speed += amount;
    console.log(`汽车的速度增加到 ${this.speed} km/h。`);
  }
}

const car = new Car(50);
car.accelerate(20); // 输出: 汽车的速度增加到 70 km/h。

// 接口不能包含属性
interface Drivable {
  drive(): void;
}

因此,如果一个类中有一些通用方法,可以通过抽象类提供部分实现,子类继承这些方法。接口适合用于定义一组行为规范,确保实现该接口的类提供特定的功能。

在实际开发中,抽象类和接口可以结合使用,充分利用它们各自的优势。

// 定义接口
interface Swimmable {
  swim(): void;
}

interface Flyable {
  fly(): void;
}

// 抽象类
abstract class Animal {
  protected name: string;

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

  abstract makeSound(): void;

  public move(): void {
    console.log(`${this.name} 正在移动。`);
  }
}

// 具体类实现多个接口并继承抽象类
class Duck extends Animal implements Swimmable, Flyable {
  constructor(name: string) {
    super(name);
  }

  public makeSound(): void {
    console.log(`${this.name} 呱呱叫!`);
  }

  public swim(): void {
    console.log(`${this.name} 正在游泳。`);
  }

  public fly(): void {
    console.log(`${this.name} 正在飞行。`);
  }
}

const duck = new Duck("小鸭子");
duck.makeSound(); // 输出: 小鸭子 呱呱叫!
duck.move(); // 输出: 小鸭子 正在移动。
duck.swim(); // 输出: 小鸭子 正在游泳。
duck.fly(); // 输出: 小鸭子 正在飞行。

Duck 类继承了 Animal 抽象类,并实现了 Swimmable 和 Flyable 接口。抽象类提供通用的行为(如 move),而接口提供具体的行为规范(如 swim 和 fly),使得代码设计更加灵活和清晰。

总结

TypeScript 的类是面向对象编程的核心,封装了对象的状态(属性)和行为(方法),支持静态成员和非静态成员的区别。类可以使用访问修饰符(publicprivateprotected)控制成员的可见性,并支持继承和方法重写来扩展父类的功能。抽象类和接口定义了行为规范,其中抽象类可提供部分实现,而接口则用于类型约定,允许一个类实现多个接口,实现灵活的代码设计。

0条评论

您的电子邮件等信息不会被公开,以下所有项均必填

OK! You can skip this field.