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

TypeScript类成员访问控制的三层策略

2021-11-224.8k 阅读

1. 类成员访问控制概述

在面向对象编程中,类成员访问控制是一项关键特性,它允许我们控制类的成员(属性和方法)在不同环境下的可访问性。这种机制有助于实现数据封装、信息隐藏以及代码的模块化和安全性。TypeScript 作为 JavaScript 的超集,继承并扩展了 JavaScript 的面向对象编程能力,提供了三层精细的类成员访问控制策略:public(公共的)、private(私有的)和 protected(受保护的)。

2. public 访问控制

2.1 public 的基本概念

public 是 TypeScript 中类成员访问控制的默认修饰符。这意味着,如果我们在定义类的属性或方法时不明确指定访问修饰符,它们默认就是 public 的。public 成员可以在类的内部、类的实例以及继承该类的子类中被访问。

2.2 public 成员的代码示例

class Animal {
    // name 属性是 public 的,因为没有显式指定修饰符
    name: string;

    constructor(name: string) {
        this.name = name;
    }

    // makeSound 方法也是 public 的
    makeSound() {
        console.log(`${this.name} makes a sound.`);
    }
}

// 创建 Animal 类的实例
const dog = new Animal('Buddy');

// 从类的实例访问 public 属性和方法
console.log(dog.name); // 输出: Buddy
dog.makeSound(); // 输出: Buddy makes a sound.

class Dog extends Animal {
    constructor(name: string) {
        super(name);
    }

    bark() {
        // 在子类中访问 public 父类成员
        console.log(`${this.name} barks.`);
    }
}

const myDog = new Dog('Max');
myDog.bark(); // 输出: Max barks.

在上述代码中,Animal 类的 name 属性和 makeSound 方法都是 public 的。我们可以在创建 Animal 类的实例 dog 后,直接访问 name 属性并调用 makeSound 方法。同时,在继承自 AnimalDog 类中,也能够在 bark 方法里访问从父类继承而来的 name 属性。

2.3 public 的应用场景

public 访问控制适用于那些希望在类的外部广泛使用的成员。例如,提供给其他开发者使用的 API 接口通常会被定义为 public。这样,外部代码可以轻松地与这些类进行交互,利用类所提供的功能,而无需关心类内部的具体实现细节。

3. private 访问控制

3.1 private 的基本概念

private 修饰符用于指定类的成员只能在类的内部访问。这意味着,无论是类的实例还是继承该类的子类,都无法直接访问 private 成员。private 成员对于外部世界来说是完全隐藏的,只有类自身的方法能够操作它们。

3.2 private 成员的代码示例

class BankAccount {
    private balance: number;

    constructor(initialBalance: number) {
        this.balance = initialBalance;
    }

    deposit(amount: number) {
        if (amount > 0) {
            this.balance += amount;
            console.log(`Deposited ${amount}. New balance: ${this.balance}`);
        } else {
            console.log('Invalid deposit amount.');
        }
    }

    withdraw(amount: number) {
        if (amount > 0 && amount <= this.balance) {
            this.balance -= amount;
            console.log(`Withdrew ${amount}. New balance: ${this.balance}`);
        } else {
            console.log('Insufficient funds or invalid withdrawal amount.');
        }
    }
}

const account = new BankAccount(1000);
// console.log(account.balance); // 这行代码会报错,因为 balance 是 private 的
account.deposit(500); // 输出: Deposited 500. New balance: 1500
account.withdraw(200); // 输出: Withdrew 200. New balance: 1300

在这个 BankAccount 类中,balance 属性被声明为 private。因此,我们不能从类的实例 account 直接访问 balance 属性。然而,类内部的 depositwithdraw 方法可以对 balance 进行操作,从而保证了账户余额的安全性和一致性。

3.3 private 的应用场景

private 访问控制主要用于保护类的内部状态和实现细节。例如,在一个数据库访问类中,我们可能有一些 private 方法来处理数据库连接字符串的生成或执行原始的 SQL 查询。这些细节对于使用该数据库访问类的外部代码来说并不重要,而且直接暴露可能会导致安全风险或代码的不稳定性。通过将这些成员设置为 private,我们可以确保只有类自身的逻辑能够正确地与这些内部机制进行交互。

4. protected 访问控制

4.1 protected 的基本概念

protected 修饰符介于 publicprivate 之间。protected 成员可以在类的内部以及继承该类的子类中访问,但不能在类的实例外部访问。这使得 protected 成员在类的继承体系中提供了一种中间级别的访问控制,既保护了成员不被外部随意访问,又允许子类根据需要进行扩展和使用。

4.2 protected 成员的代码示例

class Shape {
    protected color: string;

    constructor(color: string) {
        this.color = color;
    }

    protected getColor() {
        return this.color;
    }
}

class Circle extends Shape {
    private radius: number;

    constructor(color: string, radius: number) {
        super(color);
        this.radius = radius;
    }

    getArea() {
        return Math.PI * this.radius * this.radius;
    }

    printInfo() {
        // 在子类中访问 protected 父类成员
        console.log(`Circle with color ${this.getColor()} and area ${this.getArea()}`);
    }
}

const circle = new Circle('red', 5);
// console.log(circle.color); // 这行代码会报错,因为 color 是 protected 的
// console.log(circle.getColor()); // 这行代码也会报错,因为 getColor 是 protected 的
circle.printInfo(); // 输出: Circle with color red and area 78.53981633974483

在上述代码中,Shape 类的 color 属性和 getColor 方法被声明为 protected。在 Circle 类中,我们可以在 printInfo 方法里访问 getColor 方法来获取颜色信息,尽管 color 属性和 getColor 方法不能在 Circle 类的实例外部直接访问。

4.3 protected 的应用场景

protected 访问控制常用于基类中,当我们希望某些成员对于子类是可见且可访问的,但又不想暴露给外部代码时,就可以使用 protected。例如,在一个图形绘制库中,Shape 基类可能有一些 protected 的属性和方法,用于定义图形的通用特征和操作。子类如 CircleRectangle 等可以继承这些 protected 成员,并基于它们实现特定图形的功能,同时保证这些内部细节不会被外部随意篡改。

5. 访问控制与类型兼容性

5.1 不同访问控制修饰符对类型兼容性的影响

在 TypeScript 中,类型兼容性主要基于结构类型系统。然而,访问控制修饰符也会对类型兼容性产生一定影响。当涉及到 public 成员时,只要两个类型具有相同的 public 成员结构,它们就是兼容的。例如:

class A {
    public prop: number;
}

class B {
    public prop: number;
}

let a: A = new A();
let b: B = new B();

a = b; // 这是允许的,因为 A 和 B 的 public 成员结构相同

对于 privateprotected 成员,情况有所不同。如果两个类型具有 privateprotected 成员,那么只有当它们来自同一个声明时才是兼容的。例如:

class C {
    private secret: string;

    constructor(secret: string) {
        this.secret = secret;
    }
}

class D {
    private secret: string;

    constructor(secret: string) {
        this.secret = secret;
    }
}

let c: C = new C('abc');
let d: D = new D('def');

// c = d; // 这行代码会报错,因为 C 和 D 的 private 成员虽然结构相同,但不是来自同一个声明

而对于 protected 成员:

class E {
    protected value: number;

    constructor(value: number) {
        this.value = value;
    }
}

class F extends E {
    constructor(value: number) {
        super(value);
    }
}

class G {
    protected value: number;

    constructor(value: number) {
        this.value = value;
    }
}

let e: E = new E(10);
let f: F = new F(20);
let g: G = new G(30);

e = f; // 这是允许的,因为 F 继承自 E,它们的 protected 成员来自同一个声明
// e = g; // 这行代码会报错,因为 E 和 G 的 protected 成员不是来自同一个声明

这种规则确保了在类型兼容性检查中,privateprotected 成员的访问控制特性得到尊重,进一步加强了代码的安全性和可靠性。

6. 访问控制与模块系统

6.1 模块内的访问控制

在 TypeScript 中,模块是一种将代码组织成独立单元的方式。模块内的类成员访问控制同样遵循 publicprivateprotected 的规则。例如,在一个模块文件 example.ts 中:

class InternalClass {
    private internalProp: string;

    constructor() {
        this.internalProp = 'Internal value';
    }

    public getInternalProp() {
        return this.internalProp;
    }
}

export class ExternalClass {
    private externalProp: string;

    constructor() {
        this.externalProp = 'External value';
    }

    public getExternalProp() {
        return this.externalProp;
    }
}

在这个模块中,InternalClassinternalPropprivate 的,只能在 InternalClass 内部访问。而 ExternalClassexternalProp 也是 private 的,只能在 ExternalClass 内部访问。getInternalPropgetExternalProp 方法作为 public 接口,允许在模块外部间接访问这些 private 属性。

6.2 模块间的访问控制

当一个模块导出类时,其他模块导入该类后,对类成员的访问同样受到访问控制修饰符的限制。例如,在另一个模块 main.ts 中:

import { ExternalClass } from './example';

const external = new ExternalClass();
console.log(external.getExternalProp()); // 输出: External value
// console.log(external.externalProp); // 这行代码会报错,因为 externalProp 是 private 的

这里,虽然 ExternalClass 被导入到 main.ts 模块中,但由于 externalPropprivate 的,我们只能通过 getExternalProp 这个 public 方法来访问它。

6.3 使用访问控制保护模块内部实现

通过合理使用访问控制修饰符,我们可以有效地保护模块的内部实现细节,防止外部模块对其进行不合理的访问和修改。这有助于提高模块的独立性和可维护性,使得模块之间的依赖关系更加清晰和可控。例如,一个数据库连接模块可能有一些 private 方法来管理数据库连接池的内部状态,这些方法对于使用该模块进行数据库操作的外部代码来说是不应该被直接调用的,从而保证了数据库连接的稳定性和安全性。

7. 最佳实践与注意事项

7.1 合理选择访问控制修饰符

在设计类时,应根据类成员的用途和预期的访问范围,仔细选择合适的访问控制修饰符。对于那些需要被广泛使用的接口,应使用 public;对于仅用于类内部逻辑的成员,应使用 private;而当希望子类能够访问但外部不可访问时,应使用 protected。过度使用 public 可能会暴露过多的实现细节,增加代码的耦合度和维护成本;而过度使用 private 可能会导致代码的灵活性不足,难以进行扩展。

7.2 避免滥用 protected

虽然 protected 在类的继承体系中有其独特的作用,但也应避免滥用。如果一个成员在子类中的使用频率不高,或者可以通过其他方式(如 public 接口)间接访问,那么使用 protected 可能会增加代码的复杂性。在决定使用 protected 之前,应仔细权衡子类对该成员的实际需求以及可能带来的维护成本。

7.3 注意访问控制与代码重构

在进行代码重构时,要特别注意访问控制修饰符的变化。例如,如果将一个 private 成员提升到基类并改为 protected,可能会影响到所有子类的访问权限。同样,如果将一个 protected 成员改为 private,可能会导致子类无法访问该成员,从而引发编译错误。因此,在重构过程中,应全面评估访问控制修饰符的调整对整个代码库的影响,确保代码的正确性和稳定性。

7.4 结合文档说明访问控制策略

为了让其他开发者更好地理解类的访问控制策略,应结合代码注释或专门的文档说明每个类成员的访问权限和用途。这样可以减少误解,提高代码的可读性和可维护性。例如,在类的定义上方,可以添加注释说明哪些成员是 public 接口,哪些是 privateprotected 的内部实现细节。

8. 与其他编程语言访问控制的比较

8.1 与 Java 的访问控制比较

Java 和 TypeScript 都提供了类似的三层访问控制策略。在 Java 中,publicprivateprotected 的含义与 TypeScript 基本相同。public 成员可以在任何地方访问,private 成员只能在类内部访问,protected 成员可以在类内部和子类中访问。然而,Java 还有一个默认的包访问权限(如果不指定任何修饰符),该权限允许同一包内的类访问成员,而 TypeScript 没有类似的基于包的访问控制概念,它主要基于模块和类的结构。

8.2 与 C++ 的访问控制比较

C++ 的访问控制也包括 publicprivateprotectedpublic 成员的访问规则与 TypeScript 和 Java 一致。private 成员只能在类内部访问,protected 成员可以在类内部和子类中访问。但 C++ 在继承方式上会影响基类成员的访问权限,例如,使用 private 继承时,基类的 publicprotected 成员在子类中会变为 private。而 TypeScript 继承不改变基类成员的访问控制修饰符,这使得 TypeScript 的继承访问控制相对简单和直观。

8.3 从其他语言迁移到 TypeScript 时的注意事项

对于从 Java 或 C++ 等语言迁移到 TypeScript 的开发者,需要注意 TypeScript 与原语言在访问控制细节上的差异。例如,在 Java 中使用包访问权限的代码,在 TypeScript 中可能需要通过模块和更严格的访问控制修饰符来实现类似的功能。在 C++ 中依赖继承方式改变访问权限的代码,在 TypeScript 中需要重新设计以适应其继承访问控制规则。同时,由于 TypeScript 是 JavaScript 的超集,还需要考虑 JavaScript 的动态特性与访问控制机制的结合,避免在运行时出现意外的访问问题。

通过深入理解 TypeScript 的类成员访问控制的三层策略,开发者可以更好地设计出安全、可维护且灵活的面向对象代码,充分发挥 TypeScript 在大型项目开发中的优势。无论是构建 Web 应用、服务器端应用还是复杂的软件系统,合理运用访问控制机制都是确保代码质量和稳定性的重要环节。在实际开发中,不断积累经验,结合项目需求和最佳实践,能够更加熟练地运用这些特性,打造出高质量的软件产品。