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

TypeScript中public private protected访问修饰符解析

2024-01-087.0k 阅读

1. 访问修饰符简介

在面向对象编程中,访问修饰符(Access Modifiers)是一种用来控制类的成员(属性和方法)访问权限的机制。这种机制在不同编程语言中都有体现,比如Java、C# 等,TypeScript 作为 JavaScript 的超集,引入了访问修饰符,使得 JavaScript 也具备了更强大的面向对象编程能力。在 TypeScript 中,主要有 publicprivateprotected 这三种访问修饰符,它们分别定义了不同级别的访问权限,从而帮助开发者更好地组织代码结构、提高代码的安全性和可维护性。

2. public 访问修饰符

2.1 基本概念

public 是 TypeScript 中最宽松的访问修饰符。当一个类的成员(属性或方法)被声明为 public 时,意味着该成员可以在类的内部、类的实例以及任何外部代码中被访问。在 TypeScript 中,如果没有显式指定访问修饰符,默认情况下,类的成员就是 public 的。

2.2 代码示例

class Animal {
    public name: string;
    public constructor(name: string) {
        this.name = name;
    }
    public speak(): void {
        console.log(`${this.name} makes a sound.`);
    }
}

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

// 访问 public 属性
console.log(dog.name); // 输出: Buddy

// 调用 public 方法
dog.speak(); // 输出: Buddy makes a sound.

在上述代码中,Animal 类的 name 属性和 speak 方法都被显式声明为 public,但即便不写 public,它们默认也是 public 的。外部代码可以直接通过类的实例访问这些 public 成员。

2.3 应用场景

public 访问修饰符适用于那些希望在类的外部广泛使用的成员。比如,一个表示用户信息的类,其中用于获取用户基本信息的方法可能就需要设置为 public,以便其他模块能够获取用户的相关数据,进行展示或进一步处理。

3. private 访问修饰符

3.1 基本概念

private 访问修饰符用于限制类的成员只能在类的内部被访问,外部代码无法直接访问这些成员。这有助于隐藏类的内部实现细节,保护数据的完整性,同时也符合面向对象编程中封装的原则。

3.2 代码示例

class BankAccount {
    private accountNumber: string;
    private balance: number;

    public constructor(accountNumber: string, initialBalance: number) {
        this.accountNumber = accountNumber;
        this.balance = initialBalance;
    }

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

    public withdraw(amount: number): void {
        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.');
        }
    }
}

// 创建 BankAccount 类的实例
const account = new BankAccount('1234567890', 1000);

// 尝试从外部访问 private 属性
// console.log(account.accountNumber); // 报错: Property 'accountNumber' is private and only accessible within class 'BankAccount'.
// console.log(account.balance); // 报错: Property 'balance' is private and only accessible within class 'BankAccount'.

// 调用 public 方法
account.deposit(500); // 输出: Deposited 500. New balance: 1500
account.withdraw(200); // 输出: Withdrew 200. New balance: 1300

在上述代码中,BankAccount 类的 accountNumberbalance 属性被声明为 private,外部代码无法直接访问它们。但是,通过 public 方法 depositwithdraw,可以间接地操作 private 属性 balance,这样就保证了对账户余额操作的安全性和一致性。

3.3 注意事项

  1. 子类无法访问父类的 private 成员:与 protected 不同,当一个类继承自另一个类时,子类无法直接访问父类的 private 成员。
class Parent {
    private privateProperty: string = 'I am private';
}

class Child extends Parent {
    printParentProperty(): void {
        // console.log(this.privateProperty); // 报错: Property 'privateProperty' is private and only accessible within class 'Parent'.
    }
}
  1. 类型兼容性:在 TypeScript 中,当比较两个类型时,如果其中一个类型包含 private 成员,那么只有当这两个类型来自同一个声明时才认为它们是兼容的。
class A {
    private x: number;
}

class B {
    private x: number;
}

let a: A = new A();
// let b: B = a; // 报错: Type 'A' is not assignable to type 'B'. Types have separate declarations of a private property 'x'.

4. protected 访问修饰符

4.1 基本概念

protected 访问修饰符介于 publicprivate 之间。被声明为 protected 的类成员可以在类的内部以及子类中被访问,但不能在类的外部直接访问。这为代码提供了一种适度的封装,同时又允许子类对这些成员进行访问和扩展。

4.2 代码示例

class Shape {
    protected color: string;

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

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

class Circle extends Shape {
    private radius: number;

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

    public describe(): void {
        console.log(`This is a ${this.getColor()} circle with radius ${this.radius}.`);
    }
}

// 创建 Circle 类的实例
const myCircle = new Circle('red', 5);

// 尝试从外部访问 protected 属性
// console.log(myCircle.color); // 报错: Property 'color' is protected and only accessible within class 'Shape' and its subclasses.

// 尝试从外部调用 protected 方法
// console.log(myCircle.getColor()); // 报错: Property 'getColor' is protected and only accessible within class 'Shape' and its subclasses.

myCircle.describe(); // 输出: This is a red circle with radius 5.

在上述代码中,Shape 类的 color 属性和 getColor 方法被声明为 protectedCircle 类继承自 Shape 类,在 Circle 类的内部可以访问 Shape 类的 protected 成员,如 this.getColor()。但在外部,无法直接访问这些 protected 成员。

4.3 应用场景

  1. 代码复用与扩展:在设计类的继承体系时,protected 成员非常有用。比如,在一个图形绘制的库中,Shape 类作为基类,可能有一些 protected 的属性和方法,如颜色相关的操作,这些对于所有的形状(子类)都是有用的,但又不希望外部直接访问,子类可以基于这些 protected 成员进行扩展,实现各自特定的绘制逻辑。
  2. 封装与保护:与 private 相比,protected 提供了一种更灵活的封装方式。当希望子类能够访问某些内部状态或行为,但又不允许外部代码直接访问时,protected 是一个很好的选择。

5. 访问修饰符在类继承中的应用

5.1 继承中 public 成员的行为

当子类继承自父类时,父类的 public 成员在子类中仍然是 public 的,外部代码可以通过子类的实例访问这些从父类继承而来的 public 成员。

class Vehicle {
    public brand: string;

    public constructor(brand: string) {
        this.brand = brand;
    }

    public drive(): void {
        console.log(`Driving a ${this.brand} vehicle.`);
    }
}

class Car extends Vehicle {
    public model: string;

    public constructor(brand: string, model: string) {
        super(brand);
        this.model = model;
    }

    public displayInfo(): void {
        console.log(`This is a ${this.brand} ${this.model}.`);
    }
}

const myCar = new Car('Toyota', 'Corolla');

// 访问从父类继承的 public 属性
console.log(myCar.brand); // 输出: Toyota

// 调用从父类继承的 public 方法
myCar.drive(); // 输出: Driving a Toyota vehicle.

// 调用子类自身的 public 方法
myCar.displayInfo(); // 输出: This is a Toyota Corolla.

5.2 继承中 private 成员的行为

如前文所述,子类无法直接访问父类的 private 成员。这保证了父类内部实现细节的封装,子类只能通过父类提供的 publicprotected 方法来间接访问这些 private 成员。

class SecretHolder {
    private secret: string = 'This is a secret';

    public getSecret(): string {
        return this.secret;
    }
}

class SubSecretHolder extends SecretHolder {
    public tryAccessSecret(): void {
        // console.log(this.secret); // 报错: Property'secret' is private and only accessible within class 'SecretHolder'.
        console.log(this.getSecret()); // 通过父类的 public 方法访问 private 成员
    }
}

const subSecret = new SubSecretHolder();
subSecret.tryAccessSecret(); // 输出: This is a secret

5.3 继承中 protected 成员的行为

子类可以访问父类的 protected 成员,这使得子类能够基于父类的一些内部状态或行为进行扩展。同时,protected 成员在子类中仍然保持 protected 状态,外部代码无法直接访问。

class Base {
    protected baseValue: number;

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

    protected baseMethod(): void {
        console.log(`Base method with value ${this.baseValue}`);
    }
}

class Derived extends Base {
    public derivedMethod(): void {
        this.baseMethod();
        console.log(`Derived method using base value ${this.baseValue}`);
    }
}

const derivedObj = new Derived(10);

// 尝试从外部访问 protected 成员
// console.log(derivedObj.baseValue); // 报错: Property 'baseValue' is protected and only accessible within class 'Base' and its subclasses.
// derivedObj.baseMethod(); // 报错: Property 'baseMethod' is protected and only accessible within class 'Base' and its subclasses.

derivedObj.derivedMethod(); 
// 输出: 
// Base method with value 10
// Derived method using base value 10

6. 访问修饰符与 TypeScript 模块

6.1 模块中访问修饰符的作用

在 TypeScript 中,模块是一种组织代码的方式,它允许将相关的代码封装在一个独立的单元中。访问修饰符在模块中同样起着控制成员访问权限的作用。在模块内部,类的成员访问修饰符遵循前面所述的规则。当模块被导入到其他模块中时,只有 public 成员可以被外部模块访问,privateprotected 成员对于外部模块来说是不可见的。

6.2 代码示例

假设我们有一个 user.ts 模块,定义如下:

// user.ts
class User {
    private username: string;
    private password: string;

    public constructor(username: string, password: string) {
        this.username = username;
        this.password = password;
    }

    public getUsername(): string {
        return this.username;
    }
}

export const currentUser = new User('JohnDoe', 'password123');

然后在另一个 main.ts 模块中导入并使用:

// main.ts
import { currentUser } from './user';

// 尝试访问 private 属性
// console.log(currentUser.password); // 报错: Property 'password' is private and only accessible within class 'User'.

// 访问 public 方法
console.log(currentUser.getUsername()); // 输出: JohnDoe

在上述示例中,User 类的 password 属性是 private 的,在 main.ts 模块中无法访问。而 getUsername 方法是 public 的,可以被外部模块调用。

6.3 模块与封装性

通过使用访问修饰符结合模块,我们可以更好地实现代码的封装和信息隐藏。模块内部的类可以通过 privateprotected 访问修饰符来隐藏内部实现细节,只暴露 public 接口给外部模块使用。这有助于提高代码的安全性和可维护性,使得模块之间的依赖关系更加清晰,外部模块只能使用模块设计者期望的方式与模块进行交互。

7. 访问修饰符与 JavaScript 运行时

7.1 编译过程中的处理

TypeScript 是一种静态类型语言,最终会被编译成 JavaScript 代码运行在浏览器或 Node.js 环境中。在编译过程中,访问修饰符相关的信息会被移除,因为 JavaScript 本身并没有直接支持访问修饰符的概念。例如,下面的 TypeScript 代码:

class Example {
    private privateProp: number;

    public constructor() {
        this.privateProp = 10;
    }

    public getPrivateProp(): number {
        return this.privateProp;
    }
}

编译后的 JavaScript 代码如下:

var Example = /** @class */ (function () {
    function Example() {
        this.privateProp = 10;
    }
    Example.prototype.getPrivateProp = function () {
        return this.privateProp;
    };
    return Example;
})();

可以看到,private 修饰符在编译后消失了,JavaScript 代码中没有任何对访问权限的限制。

7.2 运行时的访问控制模拟

虽然编译后访问修饰符信息丢失,但在运行时,我们可以通过一些约定和闭包来模拟访问控制。例如,我们可以通过闭包来隐藏内部变量:

var Example = (function () {
    var privateProp;
    function Example() {
        privateProp = 10;
    }
    Example.prototype.getPrivateProp = function () {
        return privateProp;
    };
    return Example;
})();

var example = new Example();
// console.log(example.privateProp); // 报错: example.privateProp is not defined
console.log(example.getPrivateProp()); // 输出: 10

在上述 JavaScript 代码中,通过闭包的方式,外部代码无法直接访问 privateProp 变量,只能通过 getPrivateProp 方法来获取其值,从而模拟了类似 private 访问修饰符的效果。但这种方式与 TypeScript 中基于类型系统的访问修饰符相比,安全性和便利性都有所不足。

8. 实际项目中访问修饰符的最佳实践

8.1 合理封装与暴露接口

在实际项目中,应根据业务需求合理使用访问修饰符。对于那些不希望被外部随意修改或访问的内部状态和实现细节,应使用 privateprotected 修饰符进行封装。只暴露必要的 public 接口给外部调用,这样可以保证代码的稳定性和安全性。例如,在一个数据库操作类中,数据库连接字符串等敏感信息应该设置为 private,而提供一些 public 方法来执行查询、插入等操作。

class Database {
    private connectionString: string;

    public constructor(connectionString: string) {
        this.connectionString = connectionString;
    }

    public query(sql: string): void {
        console.log(`Executing query: ${sql} with connection ${this.connectionString}`);
    }
}

const db = new Database('mongodb://localhost:27017');
db.query('SELECT * FROM users');

8.2 继承体系中的设计

在设计类的继承体系时,要谨慎使用 protected 成员。如果一个成员在子类中有合理的使用场景,并且不希望外部直接访问,可以考虑将其设置为 protected。但过多地使用 protected 可能会破坏封装性,所以要确保子类对 protected 成员的访问是必要的,并且不会导致代码的耦合度过高。例如,在一个图形绘制库中,Shape 类的一些与绘制相关的基础方法可以设置为 protected,供子类扩展实现具体的绘制逻辑。

class Shape {
    protected color: string;

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

    protected drawBase(): void {
        console.log('Drawing basic shape structure');
    }
}

class Rectangle extends Shape {
    private width: number;
    private height: number;

    public constructor(color: string, width: number, height: number) {
        super(color);
        this.width = width;
        this.height = height;
    }

    public draw(): void {
        this.drawBase();
        console.log(`Drawing a ${this.color} rectangle with width ${this.width} and height ${this.height}`);
    }
}

8.3 避免过度封装

虽然封装是面向对象编程的重要原则,但也不应过度封装。如果将所有成员都设置为 privateprotected,使得外部代码难以与类进行交互,会降低代码的实用性。要在封装和易用性之间找到平衡,确保类的接口设计合理,既保护了内部实现,又方便外部使用。例如,一个简单的数据模型类,如果所有属性都设置为 private,且没有提供足够的 public 访问器,那么这个类在其他模块中就很难被有效使用。

class Person {
    private name: string;
    private age: number;

    // 没有提供 public 访问器
}

// 很难在外部使用 Person 类获取数据

更好的做法是提供适当的 public 访问器:

class Person {
    private name: string;
    private age: number;

    public getName(): string {
        return this.name;
    }

    public getAge(): number {
        return this.age;
    }

    public constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
    }
}

const person = new Person('Alice', 30);
console.log(person.getName()); // 输出: Alice
console.log(person.getAge()); // 输出: 30

通过遵循这些最佳实践,可以使代码在面向对象设计方面更加健壮、可维护,充分发挥 TypeScript 访问修饰符的优势。