在 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 岁了。
在上面的代码中有如下解释:
属性:name 和 age 是类的属性,它们存储了每个实例的状态(信息)。
构造函数:constructor 是类的特殊方法,用于在创建对象时初始化属性。
方法:greet() 是类的方法,用于表示对象的行为。
最终结果如下图所示:
静态成员与非静态成员的区别
在 TypeScript 中,类的成员可以分为静态成员和非静态成员。它们的主要区别在于:
非静态成员:属于每个实例对象。每创建一个新的对象,都会创建一组独立的非静态成员。
静态成员:属于类本身,而不是实例。静态成员可以通过类名直接访问,不需要实例化对象。
如下代码所示:
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 公里。
在上面的代码中有如下解释:
静态变量 totalCars 是一个类级别的属性,所有 Car 对象共享这个值。
非静态变量 mileage 是每个 Car 对象独立的属性,创建一个新对象就会有一个独立的 mileage 值。
静态方法 showTotalCars 是通过类名调用的,而不是通过对象调用的。
最终输出结果如下图所示:
为什么静态成员不能访问非静态成员?
由于静态成员属于类本身,而非静态成员属于实例。静态成员和非静态成员的生命周期不同。静态成员在类加载时已经存在,而非静态成员依赖于具体的实例对象。
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 中的访问修饰符用于控制类成员的访问权限:
public:公有成员,可以在类的外部访问(默认修饰符)。
private:私有成员,只能在类的内部访问,不能在外部或者子类中访问。
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 接口
抽象类和接口的基本概念:
抽象类:
抽象类是不能被直接实例化的类,只能作为基类被继承。
抽象类可以包含抽象方法和具体方法。抽象方法没有实现,必须由子类实现;具体方法可以在抽象类中有实现,子类可以继承这些方法。
抽象类用于定义通用的行为规范,同时可以提供部分实现,适合用于构建类的层次结构。
接口
接口是用来定义类的行为规范的,它只包含方法和属性的签名,不包含具体实现。
类可以通过 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 的类是面向对象编程的核心,封装了对象的状态(属性)和行为(方法),支持静态成员和非静态成员的区别。类可以使用访问修饰符(public
、private
、protected
)控制成员的可见性,并支持继承和方法重写来扩展父类的功能。抽象类和接口定义了行为规范,其中抽象类可提供部分实现,而接口则用于类型约定,允许一个类实现多个接口,实现灵活的代码设计。