MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

TypeScript 类中的构造函数与初始化方法

2021-08-166.5k 阅读

构造函数基础

在 TypeScript 的类中,构造函数是一个特殊的方法,用于在创建类的实例时初始化对象的状态。它的命名与类名相同,并且在使用 new 关键字创建对象实例时会自动调用。

简单构造函数示例

class Person {
    name: string;
    age: number;
    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
    }
}
let john = new Person('John', 30);
console.log(john.name); 
console.log(john.age); 

在上述代码中,Person 类有一个构造函数 constructor(name: string, age: number)。当使用 new Person('John', 30) 创建 john 实例时,构造函数被调用,nameage 参数被用来初始化 johnnameage 属性。

构造函数参数与属性声明合并

TypeScript 允许在构造函数参数中同时声明和初始化类的属性,这是一种简洁的写法。

class Animal {
    constructor(public species: string, public legs: number) {}
}
let dog = new Animal('Dog', 4);
console.log(dog.species); 
console.log(dog.legs); 

这里在构造函数参数 specieslegs 前加上 public,这不仅声明了参数,还同时声明了具有相同名称和类型的类属性,并将参数值赋给这些属性。类似地,也可以使用 privateprotected 关键字。

构造函数的继承与 super 关键字

当一个类继承自另一个类时,子类的构造函数如果存在,必须调用父类的构造函数,这就需要使用 super 关键字。

子类构造函数调用父类构造函数

class Shape {
    constructor(public color: string) {}
}
class Rectangle extends Shape {
    constructor(color: string, public width: number, public height: number) {
        super(color); 
    }
}
let redRectangle = new Rectangle('red', 10, 20);
console.log(redRectangle.color); 
console.log(redRectangle.width); 
console.log(redRectangle.height); 

Rectangle 类的构造函数中,super(color) 调用了 Shape 类的构造函数,将 color 参数传递给父类,以初始化父类的 color 属性。如果子类有构造函数但没有调用 super,TypeScript 会报错。

super 在不同场景的使用

  1. 在构造函数中调用父类构造函数:如上述例子,用于初始化从父类继承的属性。
  2. 调用父类的方法:在子类的方法中,可以使用 super 调用父类的同名方法。
class Vehicle {
    move(distance: number) {
        console.log(`Vehicle moved ${distance}m`);
    }
}
class Car extends Vehicle {
    move(distance: number) {
        super.move(distance); 
        console.log('Car moved smoothly');
    }
}
let myCar = new Car();
myCar.move(100); 

这里 Car 类重写了 move 方法,在 super.move(distance) 调用了父类的 move 方法后,又添加了自己的逻辑。

初始化方法

除了构造函数,类中还可以定义其他初始化方法,用于对对象进行更复杂的初始化操作,这些方法通常在构造函数调用后按需调用。

普通初始化方法

class Database {
    connection: string;
    constructor() {
        // 构造函数中可能只是简单初始化
        this.connection = 'default';
    }
    initializeConnection(url: string) {
        this.connection = url;
        console.log(`Connected to ${this.connection}`);
    }
}
let db = new Database();
db.initializeConnection('mongodb://localhost:27017'); 

Database 类中,构造函数只是简单地将 connection 属性初始化为 'default',而 initializeConnection 方法用于根据传入的 url 来设置实际的连接字符串,并打印连接信息。

静态初始化方法

静态方法属于类本身,而不是类的实例。静态初始化方法可以用于在类加载时进行一些初始化操作,比如初始化共享资源。

class Logger {
    static instance: Logger;
    log(message: string) {
        console.log(message);
    }
    static initialize() {
        Logger.instance = new Logger();
    }
}
Logger.initialize();
let logger = Logger.instance;
logger.log('This is a log message'); 

Logger 类中,initialize 是一个静态方法,用于创建 Logger 类的单例实例 instance。在使用 Logger 类的实例前,先调用 Logger.initialize() 进行初始化。

构造函数与初始化方法的比较

  1. 调用时机
    • 构造函数:在使用 new 关键字创建类的实例时自动调用,每个实例创建时都会执行构造函数。
    • 初始化方法:由开发者在需要的时候手动调用,可以在构造函数之后的任何时间调用,也可以多次调用(如果需要的话)。
  2. 作用
    • 构造函数:主要用于对象的基本初始化,设置对象的初始状态,比如初始化对象的属性值。它确保对象在创建时就处于一个可用的状态。
    • 初始化方法:用于更复杂的初始化逻辑,这些逻辑可能在对象创建后根据不同的条件或需求进行调用。例如,连接数据库可能需要在程序运行过程中根据配置来决定何时连接,这时就适合用初始化方法。
  3. 访问权限
    • 构造函数:通常只有 public 权限,因为需要外部使用 new 关键字创建实例。虽然理论上可以有 privateprotected 构造函数(用于实现单例模式等特殊场景),但相对较少用。
    • 初始化方法:可以有 publicprivateprotected 等不同的访问权限。private 初始化方法可用于类内部的复杂初始化步骤,而 public 初始化方法可由外部调用,以完成一些延迟或按需的初始化。

复杂场景下的构造函数与初始化方法

  1. 依赖注入场景 在一些大型应用中,可能需要将依赖项注入到类中。构造函数可以很好地用于接收这些依赖项并进行初始化。
class HttpClient {
    constructor(private baseUrl: string) {}
    get(url: string) {
        return `GET request to ${this.baseUrl}${url}`;
    }
}
class UserService {
    constructor(private http: HttpClient) {}
    fetchUser() {
        return this.http.get('/users');
    }
}
let httpClient = new HttpClient('https://api.example.com');
let userService = new UserService(httpClient);
console.log(userService.fetchUser()); 

这里 UserService 类依赖于 HttpClient 类,通过构造函数将 HttpClient 的实例注入到 UserService 中,这样 UserService 就可以使用 HttpClient 的功能来获取用户数据。

  1. 延迟初始化场景 有时对象的某些属性初始化成本较高,可能希望延迟初始化。这可以通过初始化方法来实现。
class BigData {
    data: number[];
    constructor() {}
    loadData() {
        // 模拟加载大数据
        this.data = Array.from({ length: 1000000 }, (_, i) => i + 1);
        console.log('Data loaded');
    }
    sumData() {
        if (!this.data) {
            this.loadData();
        }
        return this.data.reduce((acc, val) => acc + val, 0);
    }
}
let bigData = new BigData();
console.log(bigData.sumData()); 

BigData 类中,data 属性的初始化(模拟加载大数据)放在 loadData 方法中。sumData 方法在需要计算数据总和时,先检查 data 是否已加载,如果没有则调用 loadData 进行延迟初始化。

构造函数的重载

在 TypeScript 中,类的构造函数也支持重载。构造函数重载允许我们为类提供多个不同参数列表的构造函数,以满足不同的初始化需求。

构造函数重载示例

class Point {
    x: number;
    y: number;
    constructor(x: number, y: number);
    constructor(x: number);
    constructor(x: number, y?: number) {
        if (y!== undefined) {
            this.x = x;
            this.y = y;
        } else {
            this.x = x;
            this.y = 0;
        }
    }
}
let point1 = new Point(10, 20);
let point2 = new Point(5);
console.log(point1.x, point1.y); 
console.log(point2.x, point2.y); 

在上述 Point 类中,定义了两个构造函数重载签名 constructor(x: number, y: number);constructor(x: number);,实际的构造函数实现会根据传入参数的个数来决定如何初始化 xy 属性。

重载构造函数的注意事项

  1. 实际实现:实际的构造函数实现要能够处理所有重载签名的情况。在上面的例子中,通过检查 y 是否为 undefined 来确定使用哪种初始化逻辑。
  2. 顺序:重载签名应该放在实际构造函数实现之前,否则会导致编译错误。

初始化过程中的错误处理

在构造函数和初始化方法中,都可能会遇到错误情况,需要进行适当的错误处理。

构造函数中的错误处理

class File {
    content: string;
    constructor(path: string) {
        try {
            // 模拟读取文件
            this.content = `Content of file at ${path}`;
        } catch (error) {
            console.error(`Error reading file: ${error}`);
        }
    }
}
let file = new File('/nonexistent/file.txt'); 

File 类的构造函数中,通过 try - catch 块来捕获可能在读取文件(模拟)时发生的错误,并进行相应的错误处理(这里只是打印错误信息)。

初始化方法中的错误处理

class NetworkService {
    isConnected: boolean;
    constructor() {
        this.isConnected = false;
    }
    connect(url: string): boolean {
        try {
            // 模拟网络连接
            this.isConnected = true;
            console.log(`Connected to ${url}`);
            return true;
        } catch (error) {
            console.error(`Error connecting to ${url}: ${error}`);
            return false;
        }
    }
}
let networkService = new NetworkService();
let result = networkService.connect('https://example.com');
console.log(result); 

NetworkService 类的 connect 初始化方法中,同样使用 try - catch 块来处理网络连接(模拟)过程中可能出现的错误,并返回连接结果。

构造函数与初始化方法在设计模式中的应用

  1. 单例模式 在单例模式中,构造函数通常被设为 private,以防止外部直接创建实例,而通过一个静态方法来获取唯一的实例。这个静态方法可以包含初始化逻辑。
class Singleton {
    private static instance: Singleton;
    private data: string;
    private constructor() {
        this.data = 'Initial data';
    }
    static getInstance() {
        if (!Singleton.instance) {
            Singleton.instance = new Singleton();
        }
        return Singleton.instance;
    }
    updateData(newData: string) {
        this.data = newData;
    }
    getData() {
        return this.data;
    }
}
let singleton1 = Singleton.getInstance();
let singleton2 = Singleton.getInstance();
console.log(singleton1 === singleton2); 
singleton1.updateData('New data');
console.log(singleton2.getData()); 

这里 Singleton 类的构造函数是 private,外部无法直接创建实例。getInstance 静态方法负责创建并返回唯一的实例,同时在第一次调用时进行初始化。

  1. 工厂模式 工厂模式通常涉及一个工厂类,它的构造函数或方法用于创建其他类的实例,并可以对这些实例进行初始化。
class Product {
    name: string;
    constructor(name: string) {
        this.name = name;
    }
}
class ProductFactory {
    createProduct(type: string): Product {
        if (type === 'A') {
            return new Product('Product A');
        } else if (type === 'B') {
            return new Product('Product B');
        } else {
            throw new Error('Invalid product type');
        }
    }
}
let factory = new ProductFactory();
let productA = factory.createProduct('A');
let productB = factory.createProduct('B');
console.log(productA.name); 
console.log(productB.name); 

在这个例子中,ProductFactory 类的 createProduct 方法根据传入的类型创建不同的 Product 实例,并在创建时通过 Product 的构造函数进行初始化。

总结构造函数与初始化方法的要点

  1. 构造函数
    • 是类实例创建时自动调用的特殊方法,用于基本的对象状态初始化。
    • 可以合并属性声明与初始化,提高代码简洁性。
    • 在继承关系中,子类构造函数必须调用父类构造函数,通过 super 关键字实现。
    • 支持重载,以满足不同的初始化需求。
  2. 初始化方法
    • 由开发者手动调用,用于更复杂、延迟或按需的初始化逻辑。
    • 可以是实例方法或静态方法,具有不同的访问权限,以控制访问范围。
    • 在构造函数和初始化方法中都要注意错误处理,确保程序的稳定性。
  3. 两者关系
    • 构造函数为对象提供初始状态,初始化方法在对象创建后进一步完善对象状态。
    • 合理使用构造函数和初始化方法,结合设计模式,可以构建出更灵活、可维护的 TypeScript 应用程序。

在实际的前端开发中,无论是构建小型的组件还是大型的应用程序,理解和正确使用构造函数与初始化方法对于创建健壮、高效的代码至关重要。通过不断实践和总结,开发者可以更好地运用这些概念来优化代码结构和性能。