1. 别把 readonly 当“常量”,它是一道编译期的安全防线
我先说一个大多数刚接触 readonly 的人都会踩的坑:以为 readonly 就等于 const,等于“这个值永远不变”。其实不是。readonly 的核心语义是“在初始化之后,不允许再被赋值”,它管的是“赋值”这个动作,而不是“值本身不可变”。如果你把一个对象标记成 readonly,你仍然可以修改对象的属性,只是不能把这个变量重新指向另一个对象。
这个区别非常关键。我见过不少同事在 TypeScript 里写了 readonly config: Config,然后以为 config 里的所有字段都安全了,结果运行时发现内部某个数组还是被 push 进了脏数据。原因就是 readonly 只约束了变量本身,没有约束对象的内部结构。要真正做到深层只读,得配合 Readonly<T>、as const 或者递归冻结方案。这也是为什么我说 readonly 更像是一道“编译期的安全防线”,而不是“运行时的保险箱”。
那这篇博客到底要讲什么?我会从 readonly 在不同语言里的真实面貌讲起,重点放在 TypeScript 和 C# 这两个最常见的场景上,顺带提一下 JavaScript 里模拟只读的几种惯用法,最后结合我实际项目里踩过的坑,聊聊“什么时候该用 readonly、什么时候不该用”。无论你是在写前端、后端还是全栈,只要你的代码里出现过“这个变量不该被改”的想法,这篇文章都值得你花十分钟看完。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 先从语言层面看清 readonly 的不同“变体”
很多人的困惑来自于把不同语言的同名关键字混为一谈。readonly 在 TypeScript、C#、C++ 里都存在,但语义差别不小。我整理了一张表,先帮你把底层逻辑捋清楚。
| 语言 | readonly 作用于什么 | 初始化时机 | 深层只读? | 运行时行为 |
|---|---|---|---|---|
| TypeScript | 变量、属性、索引签名 | 声明处或构造函数中 | 默认不深,需配合 Readonly/Object.freeze | 编译期检查,运行时仅对 getter 有轻微影响 |
| C# | 字段(field) | 声明处或构造函数中 | 不深,只约束字段指向 | 编译期检查 + 运行时只读,反射可绕过 |
| C++ | 成员函数、变量 | 构造函数初始化列表 | 不深,但 const 限定可传递 | 编译期强约束,const_cast 可绕过 |
| JavaScript | 无原生 readonly | 无 | 无 | 无原生支持 |
看到没有,readonly 几乎都是“浅层约束 + 编译期强检查”的组合。它在运行时的存在感很弱,主要价值是在你敲代码的阶段就拦住“不该发生的赋值”。
2.1 TypeScript 里的 readonly:属性、数组、泛型约束
TypeScript 里 readonly 最常见的用法是修饰类的属性,或者接口(interface)里的成员。我举一个典型例子:
typescript复制interface User {
readonly id: string;
name: string;
email: string;
}
class UserService {
private readonly users: Map<string, User> = new Map();
constructor() {
this.users.set('u_001', { id: 'u_001', name: '张三', email: 'zhangsan@example.com' });
}
updateEmail(id: string, newEmail: string) {
const user = this.users.get(id);
if (user) {
// 这里合法:user 是从 Map 里取出来的,readonly 约束的是 users 这个属性的指向
user.email = newEmail;
}
}
}
注意我上面写的注释——users 是 readonly,意味着你不能 this.users = new Map(),但你完全可以通过 this.users.set() 往里塞数据。这是 TypeScript readonly 最容易被误解的地方。
如果你真想保护数组不被修改,有两个选择:一是用 ReadonlyArray<T>,二是直接标注 readonly 元组:
typescript复制const statusList: readonly string[] = ['active', 'inactive', 'pending'];
// statusList.push('deleted') // 报错:类型“readonly string[]”上不存在属性“push”
const roleTuple: readonly [string, number] = ['admin', 1];
// roleTuple[0] = 'user' // 报错:无法分配到 '0',因为它是只读属性
在泛型约束里,readonly 也有一个常见误用:你没法直接用 T extends readonly T[] 这种写法去描述“一
