1. TypeScript 类的核心概念
TypeScript 类是面向对象编程的基础构建块,它扩展了 JavaScript 的类特性,提供了更严格的类型检查和更丰富的面向对象功能。与 JavaScript 类相比,TypeScript 类最大的区别在于其静态类型系统,这使得我们能够在编译阶段就发现潜在的类型错误。
一个最基本的 TypeScript 类定义如下:
typescript复制class Person {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
greet() {
return `Hello, my name is ${this.name} and I'm ${this.age} years old.`;
}
}
在这个例子中,我们明确声明了类属性的类型(name: string 和 age: number),这是 TypeScript 特有的语法。而在 JavaScript 中,这些类型声明是不存在的。
1.1 类成员的访问修饰符
TypeScript 提供了三种访问修饰符来控制类成员的可见性:
- public:默认修饰符,成员可以在任何地方被访问
- private:成员只能在类内部被访问
- protected:成员可以在类及其子类中被访问
typescript复制class Employee {
public name: string; // 可以在任何地方访问
private salary: number; // 只能在 Employee 类内部访问
protected department: string; // 可以在 Employee 及其子类中访问
constructor(name: string, salary: number, department: string) {
this.name = name;
this.salary = salary;
this.department = department;
}
}
在实际项目中,我建议尽可能使用 private 和 protected 修饰符,这有助于封装内部实现细节,减少意外的外部访问,提高代码的健壮性。
1.2 构造函数参数属性
TypeScript 提供了一种简写语法,可以直接在构造函数参数中声明和初始化类属性:
typescript复制class Point {
constructor(public x: number, public y: number) {
// 自动创建并初始化 this.x 和 this.y
}
}
这种语法糖可以显著减少样板代码,特别是在需要声明多个属性的类中。不过要注意,过度使用这种语法可能会降低代码的可读性,特别是当构造函数逻辑变得复杂时。
2. 类的继承与多态
TypeScript 支持基于类的继承,这是面向对象编程的核心概念之一。通过继承,子类可以获得父类的属性和方法,并可以添加或覆盖它们。
2.1 基本继承语法
typescript复制class Animal {
constructor(public name: string) {}
move(distance: number = 0) {
console.log(`${this.name} moved ${distance}m.`);
}
}
class Dog extends Animal {
bark() {
console.log('Woof! Woof!');
}
}
const dog = new Dog('Buddy');
dog.bark(); // Woof! Woof!
dog.move(10); // Buddy moved 10m.
在这个例子中,Dog 类继承了 Animal 类,因此 Dog 的实例可以访问 Animal 的 move 方法,同时也有自己特有的 bark 方法。
2.2 方法重写与 super 关键字
子类可以重写父类的方法,如果需要调用父类的实现,可以使用 super 关键字:
typescript复制class Cat extends Animal {
constructor(name: string, private lives: number) {
super(name); // 必须调用父类的构造函数
}
move(distance: number = 5) {
console.log('Moving like a cat...');
super.move(distance); // 调用父类的 move 方法
}
}
注意:在子类构造函数中,必须在访问 this 之前调用 super(),这是 TypeScript 的强制要求。
2.3 抽象类
TypeScript 支持抽象类,这些类不能被直接实例化,只能被继承:
typescript复制abstract class Shape {
abstract getArea(): number; // 抽象方法,必须在子类中实现
printArea() {
console.log(`Area: ${this.getArea()}`);
}
}
class Circle extends Shape {
constructor(private radius: number) {
super();
}
getArea() {
return Math.PI * this.radius ** 2;
}
}
抽象类在定义公共接口和部分实现时非常有用,特别是在设计大型应用程序的架构时。我在实际项目中经常使用抽象类来定义核心业务逻辑的骨架,让具体实现由子类完成。
3. 类与接口
TypeScript 的接口不仅可以描述对象形状,还可以用于描述类的公共部分。这是 TypeScript 特有的功能,JavaScript 中没有直接对应的概念。
3.1 类实现接口
typescript复制interface ClockInterface {
currentTime: Date;
setTime(d: Date): void;
}
class Clock implements ClockInterface {
currentTime: Date = new Date();
setTime(d: Date) {
this.currentTime = d;
}
}
当一个类实现一个接口时,TypeScript 会检查类是否正确地实现了接口中定义的所有成员。这种机制在构建大型系统时特别有用,可以确保不同的类遵循相同的契约。
3.2 构造函数接口
TypeScript 还允许我们定义构造函数接口:
typescript复制interface ClockConstructor {
new (hour: number, minute: number): ClockInterface;
}
function createClock(ctor: ClockConstructor, hour: number, minute: number): ClockInterface {
return new ctor(hour, minute);
}
这种高级用法在工厂模式或依赖注入场景中非常有用。不过要注意,这种语法可能会让初学者感到困惑,建议在团队项目中谨慎使用,并确保有良好的文档说明。
4. 高级类特性
4.1 静态成员
类可以有静态成员,这些成员属于类本身而不是类的实例:
typescript复制class Grid {
static origin = {x: 0, y: 0};
constructor(public scale: number) {}
calculateDistanceFromOrigin(point: {x: number; y: number}) {
const xDist = point.x - Grid.origin.x;
const yDist = point.y - Grid.origin.y;
return Math.sqrt(xDist * xDist + yDist * yDist) * this.scale;
}
}
静态成员常用于工具函数或常量值。在我的项目中,我经常使用静态方法来创建工厂方法或提供实用功能。
4.2 只读属性
TypeScript 允许将属性标记为 readonly,这表示属性只能在声明时或构造函数中被初始化:
typescript复制class Octopus {
readonly name: string;
readonly numberOfLegs: number = 8;
constructor(name: string) {
this.name = name;
}
}
只读属性在表示不可变数据时非常有用,可以帮助我们编写更安全、更可预测的代码。
4.3 参数属性
我们之前提到过参数属性,这里再深入探讨一下。参数属性是一种简写语法,可以同时声明和初始化成员:
typescript复制class Animal {
constructor(private name: string, protected age: number) {}
getName() {
return this.name;
}
}
这种语法等同于:
typescript复制class Animal {
private name: string;
protected age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
getName() {
return this.name;
}
}
参数属性可以显著减少样板代码,但要注意不要过度使用,特别是在构造函数逻辑复杂的情况下,可能会降低代码的可读性。
4.4 存取器(getters/setters)
TypeScript 支持使用 getters 和 setters 来控制对对象成员的访问:
typescript复制class Employee {
private _fullName: string = '';
get fullName(): string {
return this._fullName;
}
set fullName(newName: string) {
if (newName && newName.length > 0) {
this._fullName = newName;
} else {
throw new Error('Invalid name');
}
}
}
存取器在需要验证或转换数据时非常有用。不过要注意,在 TypeScript 中,如果只定义了 getter 而没有 setter,属性会自动被视为 readonly。
5. 类与类型的关系
在 TypeScript 中,类定义实际上做了两件事:
- 创建了一个构造函数(运行时)
- 创建了一个类型(编译时)
这意味着我们可以像使用接口一样使用类作为类型:
typescript复制class Point {
x: number;
y: number;
}
function printPoint(p: Point) {
console.log(p.x, p.y);
}
const point = {x: 10, y: 20};
printPoint(point); // 可以工作,因为 point 的形状与 Point 类匹配
这种特性使得 TypeScript 的类型系统非常灵活。不过要注意,虽然结构类型系统允许这种"鸭子类型"的行为,但在某些情况下可能会导致意外的类型兼容性。
6. 装饰器与元数据
TypeScript 支持实验性的装饰器语法,可以用来修改类及其成员的行为:
typescript复制function sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
@sealed
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}
装饰器是一种高级特性,常用于框架开发(如 Angular)。在普通业务代码中,我建议谨慎使用装饰器,因为它们可能会增加代码的复杂性。
7. 类与模块
在现代 TypeScript 项目中,我们通常使用模块来组织代码。类可以很好地与模块系统配合:
typescript复制// shapes.ts
export class Circle {
constructor(public radius: number) {}
area() {
return Math.PI * this.radius ** 2;
}
}
// app.ts
import { Circle } from './shapes';
const circle = new Circle(5);
console.log(circle.area());
模块化的类组织方式使得代码更易于维护和重用。在实际项目中,我建议遵循单一职责原则,每个类应该只做一件事,并且做好它。
8. 类与泛型
TypeScript 的类可以与泛型结合使用,创建可重用的组件:
typescript复制class GenericNumber<T> {
zeroValue: T;
add: (x: T, y: T) => T;
}
const myNumber = new GenericNumber<number>();
myNumber.zeroValue = 0;
myNumber.add = (x, y) => x + y;
const myString = new GenericNumber<string>();
myString.zeroValue = '';
myString.add = (x, y) => x + y;
泛型类在创建数据结构(如集合、队列、栈等)时特别有用。它们允许我们编写类型安全的代码,同时保持灵活性。
9. 类与命名空间
虽然现代 TypeScript 项目更倾向于使用模块,但命名空间仍然可以与类一起使用:
typescript复制namespace Geometry {
export class Point {
constructor(public x: number, public y: number) {}
}
export class Circle {
constructor(public center: Point, public radius: number) {}
}
}
const point = new Geometry.Point(0, 0);
const circle = new Geometry.Circle(point, 10);
命名空间可以帮助组织相关的类和接口,特别是在大型代码库中。不过,对于新项目,我建议优先考虑使用模块而不是命名空间。
10. 类的最佳实践
基于多年 TypeScript 开发经验,我总结了以下类的最佳实践:
- 单一职责原则:每个类应该只有一个职责,避免创建"上帝对象"。
- 优先组合而非继承:继承会导致紧耦合,组合通常更灵活。
- 使用适当的访问修饰符:尽可能限制成员的可见性,提高封装性。
- 避免过深的继承层次:深层次的继承关系难以理解和维护。
- 考虑使用工厂方法:复杂的对象创建逻辑可以封装在工厂方法中。
- 文档化公共API:使用 JSDoc 或其他文档工具记录类的公共接口。
- 保持类的小型化:大型类难以测试和维护,考虑拆分。
- 避免过度使用静态成员:静态成员会导致全局状态,可能引发问题。
- 考虑使用接口定义契约:接口比具体类更灵活,更适合定义API。
- 编写单元测试:良好的测试覆盖率可以确保类的行为符合预期。
在实际项目中,我发现遵循这些原则可以显著提高代码质量和可维护性。特别是单一职责原则,它可能是最重要的面向对象设计原则之一。
