在 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
)控制成员的可见性,并支持继承和方法重写来扩展父类的功能。抽象类和接口定义了行为规范,其中抽象类可提供部分实现,而接口则用于类型约定,允许一个类实现多个接口,实现灵活的代码设计。