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

TypeScript 类的访问修饰符在大型项目中的应用

2022-08-042.7k 阅读

1. 理解 TypeScript 类的访问修饰符基础

在 TypeScript 中,访问修饰符用于控制类的属性和方法的可访问性。主要有三种访问修饰符:publicprivateprotected

1.1 public 修饰符

public 是默认的访问修饰符,如果没有明确指定,类的属性和方法都是 public 的。这意味着它们可以在类的内部、子类以及类的实例外部被访问。

class Animal {
    public name: string;
    public constructor(name: string) {
        this.name = name;
    }
    public sayHello(): void {
        console.log(`Hello, I'm ${this.name}`);
    }
}

let dog = new Animal('Buddy');
dog.sayHello(); // 可以在类的实例外部调用
console.log(dog.name); // 可以在类的实例外部访问

1.2 private 修饰符

private 修饰的属性和方法只能在类的内部访问,在子类或类的实例外部访问会导致编译错误。这有助于隐藏类的内部实现细节,提高代码的安全性和封装性。

class BankAccount {
    private balance: number;
    constructor(initialBalance: number) {
        this.balance = initialBalance;
    }
    private updateBalance(amount: number): void {
        this.balance += amount;
    }
    public deposit(amount: number): void {
        this.updateBalance(amount);
        console.log(`Deposited ${amount}. New balance: ${this.balance}`);
    }
}

let account = new BankAccount(100);
// account.balance; // 编译错误:'balance' 是私有属性,只能在类内部访问
// account.updateBalance(50); // 编译错误:'updateBalance' 是私有方法,只能在类内部访问
account.deposit(50);

1.3 protected 修饰符

protected 修饰符介于 publicprivate 之间。被 protected 修饰的属性和方法可以在类的内部以及子类中访问,但不能在类的实例外部访问。

class Shape {
    protected color: string;
    constructor(color: string) {
        this.color = color;
    }
    protected getColor(): string {
        return this.color;
    }
}

class Circle extends Shape {
    constructor(color: string) {
        super(color);
    }
    public displayColor(): void {
        console.log(`The circle color is ${this.getColor()}`);
    }
}

let circle = new Circle('red');
// circle.color; // 编译错误:'color' 是受保护属性,只能在类内部或子类中访问
// circle.getColor(); // 编译错误:'getColor' 是受保护方法,只能在类内部或子类中访问
circle.displayColor();

2. 在大型项目中应用 public 修饰符

2.1 提供对外接口

在大型前端项目中,组件和模块之间需要进行交互。public 修饰符常用于定义类的对外接口,使得其他部分的代码能够方便地与该类进行交互。

例如,在一个基于 React 和 TypeScript 的 UI 组件库项目中,我们可能会有一个 Button 组件类。

import React from'react';

class Button {
    public label: string;
    public onClick: () => void;
    constructor(label: string, onClick: () => void) {
        this.label = label;
        this.onClick = onClick;
    }
    public render(): JSX.Element {
        return <button onClick={this.onClick}>{this.label}</button>;
    }
}

// 在其他组件中使用
const handleClick = () => {
    console.log('Button clicked');
};
let myButton = new Button('Click me', handleClick);
// 在 React 组件中渲染
const App: React.FC = () => {
    return <div>{myButton.render()}</div>;
};

这里,labelonClickrender 都是 public 的,其他组件可以轻松地创建 Button 实例并使用这些属性和方法来实现按钮的功能和渲染。

2.2 全局状态管理类的公开方法

在大型项目中,通常会有全局状态管理的需求。比如使用 Redux 或 MobX 进行状态管理时,相关的状态管理类可能会有一些 public 方法来获取或更新状态。

以一个简单的 MobX 状态管理类为例:

import { makeObservable, observable, action } from'mobx';

class UserStore {
    public user: { name: string; age: number } | null;
    constructor() {
        this.user = null;
        makeObservable(this, {
            user: observable,
            setUser: action
        });
    }
    public setUser(name: string, age: number): void {
        this.user = { name, age };
    }
    public getUser(): { name: string; age: number } | null {
        return this.user;
    }
}

// 在其他模块中使用
let userStore = new UserStore();
userStore.setUser('John', 30);
let currentUser = userStore.getUser();
console.log(currentUser);

这里,setUsergetUser 方法是 public 的,其他模块可以通过这些方法来更新和获取用户状态。

3. 在大型项目中应用 private 修饰符

3.1 隐藏内部实现细节

在大型前端项目中,类可能有复杂的内部逻辑和数据结构,通过 private 修饰符可以将这些实现细节隐藏起来,只暴露必要的接口给外部。

例如,在一个图片加载器类中,可能有一些内部的缓存机制和加载逻辑不希望外部直接访问。

class ImageLoader {
    private imageCache: { [url: string]: HTMLImageElement } = {};
    private loadImage(url: string): Promise<HTMLImageElement> {
        return new Promise((resolve, reject) => {
            const img = new Image();
            img.onload = () => {
                this.imageCache[url] = img;
                resolve(img);
            };
            img.onerror = reject;
            img.src = url;
        });
    }
    public getImage(url: string): Promise<HTMLImageElement> {
        if (this.imageCache[url]) {
            return Promise.resolve(this.imageCache[url]);
        }
        return this.loadImage(url);
    }
}

let loader = new ImageLoader();
// loader.imageCache; // 编译错误:'imageCache' 是私有属性
// loader.loadImage('https://example.com/image.jpg'); // 编译错误:'loadImage' 是私有方法
loader.getImage('https://example.com/image.jpg').then((img) => {
    document.body.appendChild(img);
});

这里,imageCacheloadImage 被设为 private,外部只能通过 getImage 方法来获取图片,保证了内部缓存机制和加载逻辑的安全性。

3.2 防止外部误操作

在大型项目中,有些属性和方法如果被外部随意修改可能会导致程序出现严重错误。通过 private 修饰符可以有效防止这种误操作。

比如在一个游戏开发项目中,有一个 Player 类,其中 health 属性代表玩家的生命值。

class Player {
    private health: number;
    constructor(initialHealth: number) {
        this.health = initialHealth;
    }
    private damage(amount: number): void {
        this.health -= amount;
        if (this.health <= 0) {
            console.log('Player died');
        }
    }
    public takeDamage(amount: number): void {
        if (amount > 0) {
            this.damage(amount);
        }
    }
}

let player = new Player(100);
// player.health = -10; // 编译错误:'health' 是私有属性
// player.damage(50); // 编译错误:'damage' 是私有方法
player.takeDamage(50);

这里,healthdamage 被设为 private,外部只能通过 takeDamage 方法来让玩家受到伤害,避免了外部直接修改 health 导致不合理的数值出现。

4. 在大型项目中应用 protected 修饰符

4.1 子类复用与扩展

在大型前端项目的代码架构设计中,继承是一种重要的复用手段。protected 修饰符使得子类能够复用父类的一些属性和方法,同时又不会将这些细节暴露给外部。

以一个图形绘制库项目为例,有一个 Shape 父类和 Rectangle 子类。

class Shape {
    protected position: { x: number; y: number };
    constructor(x: number, y: number) {
        this.position = { x, y };
    }
    protected move(x: number, y: number): void {
        this.position.x += x;
        this.position.y += y;
    }
    public draw(): void {
        console.log(`Drawing shape at (${this.position.x}, ${this.position.y})`);
    }
}

class Rectangle extends Shape {
    private width: number;
    private height: number;
    constructor(x: number, y: number, width: number, height: number) {
        super(x, y);
        this.width = width;
        this.height = height;
    }
    public draw(): void {
        this.move(10, 10);
        console.log(`Drawing rectangle at (${this.position.x}, ${this.position.y}) with width ${this.width} and height ${this.height}`);
    }
}

let rectangle = new Rectangle(0, 0, 100, 50);
// rectangle.position; // 编译错误:'position' 是受保护属性
// rectangle.move(5, 5); // 编译错误:'move' 是受保护方法
rectangle.draw();

这里,positionmoveprotected 的,Rectangle 子类可以使用它们进行功能扩展,而外部无法直接访问这些属性和方法。

4.2 框架特定类的保护成员

在一些大型前端框架(如 Vue.js 或 Angular)的开发中,自定义组件类可能继承自框架提供的基础类。这些基础类可能有一些 protected 成员,供子类组件复用和定制。

以 Vue.js 为例,假设我们有一个自定义的 BaseComponent 类,继承自 Vue 的基础类,并且有一些 protected 方法。

import Vue from 'vue';

class BaseComponent extends Vue {
    protected dataProcessing(): void {
        // 一些数据处理逻辑
        console.log('Data processing in base component');
    }
}

class MyComponent extends BaseComponent {
    created() {
        this.dataProcessing();
    }
}

// 在 Vue 实例中使用
new MyComponent().$mount('#app');

这里,dataProcessingprotected 方法,MyComponent 子类可以调用它,而外部代码无法直接访问,保证了框架内部逻辑的封装性。

5. 访问修饰符与代码维护性

5.1 明确的接口与实现分离

在大型项目中,随着代码库的不断增长,明确的接口与实现分离对于代码维护至关重要。通过合理使用访问修饰符,我们可以清晰地定义类的外部接口(public 部分)和内部实现(privateprotected 部分)。

例如,在一个电子商务项目的购物车模块中,Cart 类可能有以下结构:

class Cart {
    private items: { product: string; quantity: number }[] = [];
    private calculateTotal(): number {
        let total = 0;
        for (let item of this.items) {
            // 假设每个商品有固定价格 10
            total += item.quantity * 10;
        }
        return total;
    }
    public addItem(product: string, quantity: number): void {
        this.items.push({ product, quantity });
    }
    public getTotal(): number {
        return this.calculateTotal();
    }
}

let cart = new Cart();
cart.addItem('Product A', 2);
let total = cart.getTotal();
console.log(`Total: ${total}`);

这里,addItemgetTotalpublic 接口,外部代码只需要关心如何添加商品和获取总价。而 itemscalculateTotal 是内部实现细节,被封装起来。这样,当内部计算总价的逻辑发生变化时,只要 public 接口不变,其他依赖该类的代码不需要修改,大大提高了代码的可维护性。

5.2 避免意外修改

在多人协作的大型项目中,很容易出现一个开发者不小心修改了另一个开发者依赖的内部数据或方法的情况。访问修饰符可以有效地避免这种意外修改。

比如在一个团队开发的地图应用项目中,有一个 MapMarker 类。

class MapMarker {
    private coordinates: { lat: number; lng: number };
    private updateMarkerPosition(): void {
        // 复杂的位置更新逻辑
        console.log('Marker position updated');
    }
    public setCoordinates(lat: number, lng: number): void {
        this.coordinates = { lat, lng };
        this.updateMarkerPosition();
    }
    public getCoordinates(): { lat: number; lng: number } {
        return this.coordinates;
    }
}

// 开发者 A 使用 MapMarker
let marker = new MapMarker();
marker.setCoordinates(10, 20);
let coords = marker.getCoordinates();

// 开发者 B 不小心直接修改了 coordinates
// marker.coordinates = { lat: 30, lng: 40 }; // 编译错误:'coordinates' 是私有属性
// marker.updateMarkerPosition(); // 编译错误:'updateMarkerPosition' 是私有方法

通过将 coordinatesupdateMarkerPosition 设为 private,避免了开发者 B 意外修改,保证了 MapMarker 类的行为一致性和代码的稳定性。

6. 访问修饰符与代码安全性

6.1 防止数据泄露

在大型前端项目中,尤其是涉及用户敏感数据的项目(如金融应用、医疗健康应用等),数据泄露是一个严重的问题。privateprotected 修饰符可以有效地防止内部敏感数据被外部非法访问。

例如,在一个在线银行应用的用户账户类中:

class BankAccount {
    private accountNumber: string;
    private pin: string;
    constructor(accountNumber: string, pin: string) {
        this.accountNumber = accountNumber;
        this.pin = pin;
    }
    public transfer(amount: number, recipientAccount: string): void {
        // 转账逻辑,这里假设验证 pin 等操作都在内部进行
        console.log(`Transferring ${amount} to ${recipientAccount}`);
    }
}

let account = new BankAccount('1234567890', '1234');
// account.accountNumber; // 编译错误:'accountNumber' 是私有属性
// account.pin; // 编译错误:'pin' 是私有属性
account.transfer(100, '0987654321');

这里,accountNumberpin 是敏感数据,被设为 private,外部无法直接访问,保护了用户的账户安全。

6.2 防止恶意篡改

在大型项目中,恶意用户或恶意代码可能试图篡改程序的关键数据或行为。通过合理使用访问修饰符,可以增强代码的安全性,防止这种恶意篡改。

比如在一个游戏服务器端的角色类中(虽然这是后端示例,但前端同样适用类似逻辑):

class GameCharacter {
    private health: number;
    private level: number;
    constructor(health: number, level: number) {
        this.health = health;
        this.level = level;
    }
    private levelUp(): void {
        this.level++;
        this.health += 10;
    }
    public attack(enemy: GameCharacter): void {
        // 攻击逻辑,例如减少敌人的生命值
        enemy.health -= 10;
        if (enemy.health <= 0) {
            this.levelUp();
        }
    }
}

let player = new GameCharacter(100, 1);
let enemy = new GameCharacter(50, 1);
// player.health = 9999; // 编译错误:'health' 是私有属性
// player.level = 99; // 编译错误:'level' 是私有属性
// player.levelUp(); // 编译错误:'levelUp' 是私有方法
player.attack(enemy);

这里,healthlevellevelUp 被设为 private,外部无法直接篡改角色的生命值、等级以及升级逻辑,保证了游戏的公平性和安全性。

7. 访问修饰符的最佳实践与注意事项

7.1 最小化公开暴露

尽量减少 public 属性和方法的数量,只公开那些外部真正需要使用的接口。过多的 public 暴露会破坏封装性,增加代码维护的难度。

例如,在一个文件上传组件类中,可能只需要公开 upload 方法和一些状态属性(如 uploadProgress)。

class FileUploader {
    private file: File | null;
    private uploadUrl: string;
    public uploadProgress: number;
    constructor(uploadUrl: string) {
        this.file = null;
        this.uploadUrl = uploadUrl;
        this.uploadProgress = 0;
    }
    private startUpload(): void {
        // 实际的上传逻辑
        console.log('Uploading file...');
    }
    public upload(file: File): void {
        this.file = file;
        this.startUpload();
    }
}

let uploader = new FileUploader('https://example.com/upload');
// uploader.file; // 不应该公开,因为外部不需要直接访问
// uploader.uploadUrl; // 不应该公开,因为外部不需要直接修改
uploader.upload(new File([], 'example.txt'));
console.log(uploader.uploadProgress);

7.2 合理使用继承和保护成员

在使用继承时,要谨慎使用 protected 修饰符。确保子类确实需要访问父类的 protected 成员,并且子类的行为与父类的设计意图一致。过度使用 protected 可能会导致代码结构混乱。

比如在一个图形几何计算库中,有一个 Shape 类和 Triangle 子类。

class Shape {
    protected sideLengths: number[];
    constructor(sideLengths: number[]) {
        this.sideLengths = sideLengths;
    }
    protected calculatePerimeter(): number {
        return this.sideLengths.reduce((sum, length) => sum + length, 0);
    }
}

class Triangle extends Shape {
    constructor(side1: number, side2: number, side3: number) {
        super([side1, side2, side3]);
    }
    public getPerimeter(): number {
        return this.calculatePerimeter();
    }
}

let triangle = new Triangle(3, 4, 5);
// 这里 Triangle 合理使用了 Shape 的 protected 成员
console.log(triangle.getPerimeter());

7.3 文档化访问修饰符的使用

在大型项目中,为了让其他开发者更好地理解代码,应该对访问修饰符的使用进行文档化。特别是对于 public 接口,要详细说明其功能、参数和返回值。

例如,使用 JSDoc 对 public 方法进行注释:

/**
 * Add an item to the shopping cart.
 * @param product - The name of the product.
 * @param quantity - The quantity of the product to add.
 */
public addItem(product: string, quantity: number): void {
    this.items.push({ product, quantity });
}

/**
 * Get the total price of items in the cart.
 * @returns The total price.
 */
public getTotal(): number {
    return this.calculateTotal();
}

这样,其他开发者在使用该类时能够清楚地了解每个 public 方法的用途。

7.4 与模块系统的结合

在 TypeScript 中,模块系统与访问修饰符相互配合。注意,即使一个类的成员是 privateprotected,在同一个模块内,这些成员可能有不同的访问规则。合理规划模块结构,确保类的封装性在模块层面也能得到保证。

例如,在一个模块 cartModule.ts 中:

class Cart {
    private items: { product: string; quantity: number }[] = [];
    public addItem(product: string, quantity: number): void {
        this.items.push({ product, quantity });
    }
}

// 在同一个模块内,虽然 items 是 private,但可以通过闭包等方式间接访问
let cart = new Cart();
// 这里不应该直接访问 items,但在模块内部可能存在一些特殊情况需要注意

要避免在模块内部意外破坏类的封装性,保持代码的一致性和可维护性。

通过合理应用 TypeScript 类的访问修饰符,在大型前端项目中可以实现更好的代码组织、维护性、安全性以及团队协作,从而打造出高质量、健壮的前端应用。无论是构建复杂的单页应用,还是大型的前端框架,对访问修饰符的深入理解和恰当使用都是至关重要的。