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

TypeScript 访问修饰符:public, private, protected 的区别与应用

2021-10-015.0k 阅读

TypeScript 访问修饰符概述

在面向对象编程中,访问修饰符(Access Modifiers)是一种非常重要的机制,它用于控制类的成员(属性和方法)的访问权限。TypeScript 作为 JavaScript 的超集,继承了面向对象编程的概念,并引入了访问修饰符,让开发者能够更精确地控制类成员的访问。TypeScript 中有三个主要的访问修饰符:publicprivateprotected。这些修饰符决定了类的成员在类的内部、子类以及类的外部如何被访问。

public 访问修饰符

public 修饰符的含义

public 是 TypeScript 中最宽松的访问修饰符。当一个类的成员(属性或方法)被标记为 public 时,意味着这个成员可以在类的内部、子类以及类的外部被访问。这是 TypeScript 的默认访问修饰符,如果没有显式指定其他修饰符,类的成员默认就是 public 的。

public 修饰符的代码示例

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

// 创建 Animal 类的实例
let dog = new Animal('Buddy');
// 访问 public 属性
console.log(dog.name); 
// 调用 public 方法
dog.sayHello(); 

在上述代码中,Animal 类的 name 属性和 sayHello 方法都被显式标记为 public(虽然不写 public 也是默认 public)。我们可以在类的外部通过 dog 实例直接访问 name 属性和调用 sayHello 方法。

public 修饰符在继承中的表现

当一个类继承自另一个类时,public 成员在子类中仍然保持 public 访问权限。

class Dog extends Animal {
    public breed: string;
    public constructor(name: string, breed: string) {
        super(name);
        this.breed = breed;
    }
    public bark(): void {
        console.log(`${this.name} is barking. Breed: ${this.breed}`);
    }
}

let goldenRetriever = new Dog('Max', 'Golden Retriever');
// 访问从父类继承的 public 属性
console.log(goldenRetriever.name); 
// 调用从父类继承的 public 方法
goldenRetriever.sayHello(); 
// 访问子类的 public 属性
console.log(goldenRetriever.breed); 
// 调用子类的 public 方法
goldenRetriever.bark(); 

在这个例子中,Dog 类继承自 Animal 类。Dog 类的实例可以访问从 Animal 类继承的 public 成员 namesayHello 方法,同时也可以访问和调用自身定义的 public 成员 breedbark 方法。

private 访问修饰符

private 修饰符的含义

private 修饰符用于限制类的成员只能在类的内部被访问。如果一个属性或方法被标记为 private,那么在类的外部,包括子类,都无法直接访问该成员。这有助于隐藏类的内部实现细节,提高代码的安全性和封装性。

private 修饰符的代码示例

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(`Withdrawn ${amount}. New balance: ${this.balance}`);
        } else {
            console.log('Insufficient funds or invalid amount');
        }
    }
}

let account = new BankAccount('1234567890', 1000);
// 以下代码会报错,因为 accountNumber 和 balance 是 private
// console.log(account.accountNumber); 
// console.log(account.balance); 
account.deposit(500);
account.withdraw(300);

在上述代码中,BankAccount 类的 accountNumberbalance 属性被标记为 private。在类的外部尝试访问这两个属性会导致编译错误。而 depositwithdraw 方法是 public,可以在类的外部调用,并且这两个方法可以在类内部访问 private 属性来执行相应的操作。

private 修饰符在继承中的表现

当一个类继承自另一个包含 private 成员的类时,子类无法直接访问父类的 private 成员。

class SavingsAccount extends BankAccount {
    public interestRate: number;
    public constructor(accountNumber: string, initialBalance: number, interestRate: number) {
        super(accountNumber, initialBalance);
        this.interestRate = interestRate;
    }
    public calculateInterest(): void {
        // 以下代码会报错,因为 balance 是 private
        // let interest = this.balance * this.interestRate;
        console.log('Calculating interest is not possible without access to balance');
    }
}

let savings = new SavingsAccount('0987654321', 2000, 0.05);
savings.deposit(100);
// savings.calculateInterest(); 会报错,因为 calculateInterest 方法内部无法访问父类的 private 成员

在这个例子中,SavingsAccount 类继承自 BankAccount 类。虽然 SavingsAccount 类可以继承 BankAccount 类的 public 方法 depositwithdraw,但它无法访问 BankAccount 类的 private 属性 balance,即使在 calculateInterest 方法内部也不行。

protected 访问修饰符

protected 修饰符的含义

protected 修饰符介于 publicprivate 之间。被标记为 protected 的类成员可以在类的内部以及子类中被访问,但在类的外部无法直接访问。这使得 protected 成员适合用于那些希望在子类中复用,但又不想暴露给外部代码的实现细节。

protected 修饰符的代码示例

class Shape {
    protected color: string;
    public constructor(color: string) {
        this.color = color;
    }
    protected calculateArea(): number {
        return 0;
    }
    public display(): void {
        let area = this.calculateArea();
        console.log(`Shape with color ${this.color} has area ${area}`);
    }
}

class Circle extends Shape {
    private radius: number;
    public constructor(color: string, radius: number) {
        super(color);
        this.radius = radius;
    }
    protected calculateArea(): number {
        return Math.PI * this.radius * this.radius;
    }
}

let redCircle = new Circle('Red', 5);
// 以下代码会报错,因为 color 是 protected
// console.log(redCircle.color); 
redCircle.display(); 

在上述代码中,Shape 类的 color 属性和 calculateArea 方法被标记为 protected。在 Shape 类的内部和 Circle 类(Shape 的子类)中,这些成员是可访问的。Circle 类重写了 calculateArea 方法,并在其中访问了从父类继承的 protected 属性 color。而在类的外部,尝试访问 color 属性会导致编译错误。

protected 修饰符在继承中的表现

当一个类继承自另一个包含 protected 成员的类时,子类可以访问父类的 protected 成员。

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;
    }
    protected calculateArea(): number {
        return this.width * this.height;
    }
}

let blueRectangle = new Rectangle('Blue', 4, 6);
blueRectangle.display(); 

在这个例子中,Rectangle 类继承自 Shape 类。Rectangle 类可以访问 Shape 类的 protected 成员 colorcalculateArea 方法,并在自己的 calculateArea 方法中进行重写和使用,以实现矩形面积的计算。

访问修饰符的应用场景

public 修饰符的应用场景

  1. 提供公共接口:当你希望类的某些成员成为类对外暴露的接口,供其他代码使用时,使用 public 修饰符。例如,一个数学计算库中的 MathUtils 类,其中的一些通用计算方法可能是 public 的,以便其他开发者在不同的项目中调用。
class MathUtils {
    public static add(a: number, b: number): number {
        return a + b;
    }
    public static subtract(a: number, b: number): number {
        return a - b;
    }
}

let result1 = MathUtils.add(5, 3);
let result2 = MathUtils.subtract(10, 4);
console.log(result1); 
console.log(result2); 
  1. 数据展示:如果类的某个属性用于展示数据,并且不涉及敏感信息,也可以将其设为 public。比如一个用于展示用户基本信息的 User 类,其中的 name 属性可以是 public
class User {
    public name: string;
    public age: number;
    public constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
    }
}

let user = new User('Alice', 30);
console.log(`Name: ${user.name}, Age: ${user.age}`); 

private 修饰符的应用场景

  1. 隐藏内部实现细节:当类的某些属性或方法是实现特定功能的内部细节,不应该被外部代码直接访问时,使用 private 修饰符。例如,一个数据库连接类 DatabaseConnection,其中用于管理数据库连接池的属性和方法可能是 private 的,外部代码只需要通过 public 方法来执行数据库操作,而无需关心连接池的具体实现。
class DatabaseConnection {
    private connectionPool: any[];
    private constructor() {
        this.connectionPool = [];
        // 初始化连接池的逻辑
    }
    private getConnection(): any {
        // 从连接池获取连接的逻辑
        return this.connectionPool.pop();
    }
    private returnConnection(connection: any): void {
        // 将连接返回连接池的逻辑
        this.connectionPool.push(connection);
    }
    public query(sql: string): void {
        let connection = this.getConnection();
        // 使用连接执行 SQL 查询的逻辑
        console.log(`Executing query: ${sql}`);
        this.returnConnection(connection);
    }
    public static getInstance(): DatabaseConnection {
        if (!DatabaseConnection.instance) {
            DatabaseConnection.instance = new DatabaseConnection();
        }
        return DatabaseConnection.instance;
    }
    private static instance: DatabaseConnection;
}

let db = DatabaseConnection.getInstance();
db.query('SELECT * FROM users');
// 以下代码会报错,因为 connectionPool 是 private
// console.log(db.connectionPool); 
  1. 保护敏感信息:如果类的某个属性包含敏感信息,如用户密码、数据库密码等,必须将其设为 private,以防止信息泄露。
class UserAccount {
    private password: string;
    public constructor(password: string) {
        this.password = password;
    }
    public login(inputPassword: string): boolean {
        return inputPassword === this.password;
    }
}

let account = new UserAccount('secretpassword');
let isLoggedIn = account.login('secretpassword');
console.log(isLoggedIn); 
// 以下代码会报错,因为 password 是 private
// console.log(account.password); 

protected 修饰符的应用场景

  1. 在子类中复用实现:当你希望父类的某些成员能够被子类复用,但又不想让外部代码直接访问时,使用 protected 修饰符。例如,一个图形绘制库中的 GraphicObject 类,其中定义了一些通用的绘制属性和方法,这些可以被 CircleRectangle 等子类继承和使用,但外部代码不需要直接访问。
class GraphicObject {
    protected lineWidth: number;
    protected color: string;
    public constructor(lineWidth: number, color: string) {
        this.lineWidth = lineWidth;
        this.color = color;
    }
    protected drawBorder(): void {
        console.log(`Drawing border with width ${this.lineWidth} and color ${this.color}`);
    }
}

class Triangle extends GraphicObject {
    private sideLength: number;
    public constructor(lineWidth: number, color: string, sideLength: number) {
        super(lineWidth, color);
        this.sideLength = sideLength;
    }
    public draw(): void {
        this.drawBorder();
        console.log(`Drawing triangle with side length ${this.sideLength}`);
    }
}

let triangle = new Triangle(2, 'Red', 5);
triangle.draw(); 
// 以下代码会报错,因为 lineWidth 和 color 是 protected
// console.log(triangle.lineWidth); 
// console.log(triangle.color); 
  1. 封装子类相关的逻辑protected 成员可以用于封装一些与子类密切相关,但不适合外部访问的逻辑。比如一个电商系统中的 Product 类,可能有一些 protected 方法用于计算产品的折扣价格,这些方法可以被子类 DigitalProductPhysicalProduct 继承和扩展,但不应该被外部代码直接调用。
class Product {
    protected price: number;
    protected discount: number;
    public constructor(price: number, discount: number) {
        this.price = price;
        this.discount = discount;
    }
    protected calculateDiscountedPrice(): number {
        return this.price * (1 - this.discount);
    }
    public displayInfo(): void {
        let discountedPrice = this.calculateDiscountedPrice();
        console.log(`Product price: ${this.price}, Discounted price: ${discountedPrice}`);
    }
}

class DigitalProduct extends Product {
    public constructor(price: number, discount: number) {
        super(price, discount);
    }
    protected calculateDiscountedPrice(): number {
        // 数字产品可能有额外的折扣逻辑
        return super.calculateDiscountedPrice() * 0.9;
    }
}

let ebook = new DigitalProduct(20, 0.1);
ebook.displayInfo(); 
// 以下代码会报错,因为 price 和 discount 是 protected
// console.log(ebook.price); 
// console.log(ebook.discount); 

访问修饰符与 TypeScript 类型兼容性

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

在 TypeScript 中,类型兼容性主要基于结构类型系统。访问修饰符在类型兼容性方面也有一定的影响。

  1. public 成员与类型兼容性:当比较两个具有 public 成员的类型时,只要两个类型的 public 成员结构一致,它们就是兼容的。例如:
class A {
    public x: number;
    public constructor(x: number) {
        this.x = x;
    }
}

class B {
    public x: number;
    public constructor(x: number) {
        this.x = x;
    }
}

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

在上述代码中,A 类和 B 类都有一个 public 属性 x。由于它们的 public 成员结构相同,所以可以将 B 类的实例赋值给 A 类的变量。

  1. privateprotected 成员与类型兼容性:如果类型中包含 privateprotected 成员,那么只有当两个类型来自同一个声明时,它们才是兼容的。例如:
class C {
    private y: string;
    public constructor(y: string) {
        this.y = y;
    }
}

class D {
    private y: string;
    public constructor(y: string) {
        this.y = y;
    }
}

let c: C = new C('abc');
// 以下代码会报错,因为 C 和 D 虽然结构相似,但 private 成员来自不同声明
// let d: D = new D('def');
// c = d; 

在这个例子中,尽管 C 类和 D 类看起来结构相同,都有一个 private 属性 y,但由于 y 来自不同的声明,它们是不兼容的,不能互相赋值。

对于 protected 成员也是类似的情况,只有当两个类型中的 protected 成员来自同一个声明时,它们才是兼容的。

总结访问修饰符的选择要点

  1. 考虑访问范围需求:如果希望类的成员能被广泛访问,包括类的外部代码,使用 public 修饰符。如果只想在类的内部访问,使用 private 修饰符。如果希望在类的内部和子类中访问,使用 protected 修饰符。
  2. 保护敏感信息和封装实现细节:对于敏感信息,如密码、密钥等,以及不希望外部代码直接访问的内部实现细节,使用 private 修饰符。对于那些在子类中有复用需求的内部逻辑,使用 protected 修饰符。
  3. 遵循面向对象设计原则:合理使用访问修饰符有助于遵循面向对象编程的封装、继承和多态原则。封装通过 privateprotected 修饰符隐藏内部细节,继承和多态可以利用不同访问修饰符在子类中的特性来实现。
  4. 注意类型兼容性:在考虑类型兼容性时,要注意 privateprotected 成员对类型兼容性的影响。确保在进行类型赋值和比较时,符合 TypeScript 的类型兼容性规则。

通过正确理解和使用 publicprivateprotected 访问修饰符,开发者可以更好地设计和组织 TypeScript 代码,提高代码的安全性、可维护性和可扩展性。在实际项目中,根据具体的需求和设计目标,谨慎选择合适的访问修饰符,将有助于构建健壮和高质量的软件系统。