在默认情况下,我们在类内部定义的属性(如 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。
注意:
示例 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);运行结果如下。
账户 ID:VIP-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 种访问修饰符,如下表所示。
| 类内部 | 子类内部 | 实例对象 | |
|---|---|---|---|
| public | √ | √ | √ |
| protected | √ | √ | × |
| private | √ | × | × |
