TypeScript 访问修饰符

在默认情况下,我们在类内部定义的属性(如 name、age),在类的外部是可以被随意访问和修改的。

想象这样一个真实的业务场景:你负责开发一个银行系统,写了一个 BankAccount(银行账户)类,里面有一个 balance(余额)属性。如果任何一个外部实例都可以直接写出 account.balance = 99999999 这种代码,那整个系统的安全防线就彻底崩溃了。

为了解决这个问题,面向对象编程引入了另一大核心特性——封装。它的本质就是把不想让外部直接碰到的核心数据藏起来,只提供安全的途径去访问。

在 TypeScript 中,我们可以通过 3 个 “访问修饰符” 来实现精准的权限控制:public、private 和 protected。

TypeScript public 访问修饰符

在 TypeScript 中,public 是类成员的默认访问修饰符。也就是说,如果我们在属性或方法前面什么都不写,那么它就是 public。

public 代表的是 “公有”。也就是说,所有被 public 修饰的属性和方法,都可以在任何地方被访问到,包括类的内部、子类的内部、实例化出来的对象等。

示例 1:使用 public 修饰符

// 银行账户:账户所有者姓名可以公开访问
class BankAccount {
    // 这里写 public 和不写 public,是一样的效果
    public name: string;

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

    public showInfo(): void {
        console.log("账户所有人:", this.name);
    }
}

const account = new BankAccount("Jack");
// 外部可以随意修改 public 属性
account.name = "黑客";
account.showInfo();

运行结果如下。

账户所有人:黑客

分析:

对于普通数据(如客户称呼)来说,public 非常方便。但试想一下,如果上面的属性是账户余额,使用裸奔的 public 就是绝对不可取的,我们需要更严格的修饰符。

TypeScript private 访问修饰符

在 TypeScript 中,private 是最严格的访问修饰符。当一个属性或方法被标记为 private 时,它就变成了这个类的 “绝对私有财产”,只能在这个类的内部被访问和修改。

也就是说,类的外部、子类的内部、实例化的对象等,都无权访问或修改 private 属性或方法。

示例 2:使用 private 修饰符

// 银行账户:余额属于核心敏感数据,必须私有化
class SafeBankAccount {
    public name: string;
    // 设置私有属性,禁止外部直接访问或篡改
    private balance: number;

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

    // 只能通过类内部的 public 方法(如存款)来安全地操作私有属性
    public deposit(amount: number): void {
        // 业务边界校验逻辑:存款金额必须大于 0
        if (amount > 0) {
            this.balance += amount;
            console.log(`${this.name} 存款成功,当前余额:${this.balance}`);
        }
    }
}

const myAccount = new SafeBankAccount("Jack", 100);
myAccount.deposit(50);

运行结果如下。

Jack 存款成功,当前余额:150

分析:

当我们使用 private 将 balance 设置为私有属性之后,外部想要存钱,就只能乖乖地去调用 deposit() 方法 。而在 deposit() 方法内部,我们可以随意加上各种业务校验逻辑 。这样就完美实现了数据的 “安全封装”。

如果在外部强行执行 myAccount.balance = 999999,TypeScript 会直接飘红报错拦截。

TypeScript protected 访问修饰符

在 TypeScript 中,protected 是一个比较巧妙的修饰符,它的限制介于 public 和 private 之间。当一个属性被标记为 protected 时:

  • 在继承它的 “子类” 眼中:它内部所有的属性和方法表现得就像 public 一样。也就是说,它允许在继承它的 “子类” 内部被访问。
  • 在实例对象眼中:它内部所有的属性和方法表现得就像 private 一样。也就是说,外部对象绝对不允许访问它。

小伙伴们可以这样去理解:protected 就像是家族的传家宝,外人不能碰,但子孙后代可以继承和使用。

示例 3:使用 protected 修饰符

class BaseEmployee {
    public name: string;
    // 薪资是敏感信息,不能对外公开,但允许子类访问以计算奖金
    protected baseSalary: number;

    constructor(name: string, salary: number) {
        this.name = name;
        this.baseSalary = salary;
    }
}

class Manager extends BaseEmployee {
    public calculateTotalPay(): void {
        // 子类内部可以合法访问父类的 protected 属性
        const bonus = this.baseSalary * 0.2;
        const total = this.baseSalary + bonus;
        console.log(`${this.name} 的总收入为:${total}`);
    }
}

const boss = new Manager("Jack", 10000);
boss.calculateTotalPay();

运行结果如下。

Jack 的总收入为:12000

分析:

在这个例子中,如果实例对象试图直接访问 protected 属性,那么 TypeScript 就会直接报错:

// 错误操作:外部对象试图直接访问 protected 属性
console.log(boss.baseSalary);
// 报错:属性 “baseSalary” 受保护,只能在类 “BaseEmployee” 及其子类中访问

TypeScript readonly 状态修饰符

在真实业务中,我们还会遇到一种特殊的属性:它在对象刚刚创建(实例化)时就被赋予了初始值,之后任何人都只许访问但不许修改,比如用户的唯一身份 ID、数据库记录的 ID 等。

实际上,除了上面 3 大访问修饰符之外,TypeScript 还提供了一个非常好用的状态修饰符:readonly。

注意:

  • readonly 是状态修饰符,并不是访问修饰符。它不仅可以用于类,还可以用于接口类型别名数组元组等地方。
  • TypeScript 访问修饰符只有 3 种:public、private、protected。

示例 4:使用 readonly 状态修饰符

// 场景:银行账户的唯一账号 ID,一旦开户分配后,绝对不允许修改
class BankAccountWithID {
    // readonly 属性,一旦初始化后就不允许修改
    public readonly id: string;
    public name: string;

    constructor(id: string, name: string) {
        this.id = id;     // readonly 属性只允许在声明时,或在 constructor() 构造函数中进行赋值
        this.name = name;
    }
}

const item = new BankAccountWithID("VIP-8888", "Jack");
console.log("账户 ID:", item.id);

运行结果如下。

账户 IDVIP-8888

分析:

在这个例子中,我们同时使用了 public 和 readonly 这两个来修饰 id 这个属性。因此该属性在外部是只能被访问,但不允许被修改的。如果我们试图修改它,TypeScript 会提示“无法为该属性赋值,因为它是只读属性” 。

如果我们尝试在外部试图修改只读属性,则 TypeScript 会直接报错,比如:

// 错误操作:试图修改只读属性
item.id = "VIP-9999";
// 报错:无法为 “id” 赋值,因为它是只读属性。

在有些项目源码中,我们还会经常看到一种把修饰符直接写进构造函数 constructor() 参数里的骚操作,比如:

// 场景:极简写法的银行账户类
class QuickAccount {
    // 只要在构造函数参数前加上修饰符(public / private / protected / readonly)
    // TypeScript 就会自动帮你声明并初始化这个属性,上面连写都不用写了
    constructor(public readonly accountId: string, private balance: number) {
    }

    print(): void {
        console.log(`账户 ${this.accountId} 的余额被安全保护,值为:${this.balance}`);
    }
}

上面这种写法其实是 TypeScript 的语法糖,它能帮你把 “声明属性 + 接收参数 + 赋值” 这三步合为一步,极大精简了代码。

TypeScript private 与 JavaScript 中 “#” 的区别

如果小伙伴们关注过最新的 ECMAScript 规范,会发现 JavaScript 官方也推出了一种表示私有属性的语法:在属性名前加 “#” 号(比如 #balance)。

那么,它和 TypeScript 中的 private 到底有什么区别呢?

  • TypeScript 的 private(软私有):它只在 “代码编写阶段(编译期)” 起作用。一旦代码被编译成普通的 JS 文件并在浏览器里运行,这个限制就消失了。如果你在运行期间用一些非常规手段,依然是可以获取并修改到这个值的。
  • JavaScript 原生的 #(硬私有):这是真正的运行时隔离。就算代码跑在浏览器里,外部无论如何也无法读取到带有 # 开头的属性。

那么,在实际开发中应该怎么选呢?

在目前的企业级 TypeScript 项目中,绝大多数团队依然更倾向于使用 TypeScript 的 private 修饰符,因为它不用加丑陋的 “#” 号,阅读体验更好,而且在编译期就已经能挡住 99% 的规范问题了。只有在对安全性要求极其变态的底层金融库中,才会考虑混用原生的 # 硬私有。

在这一节的最后,我们来比较一下TypeScript 的 3 种访问修饰符,如下表所示。

TypeScript 的 3 种修饰符
类内部 子类内部 实例对象
public
protected ×
private × ×
给站长反馈

绿叶网正在不断完善中,小伙伴们如果发现任何问题,还望多多给站长反馈,谢谢!

邮箱:lvyenet@vip.qq.com

「绿叶网」服务号
绿叶网服务号放大
关注服务号,微信也能看教程。
绿叶网服务号