只读与参数属性
本教程共 80 篇 · 第 32 篇 · 更新于 2026-08-10 · 约 9 分钟阅读
本节目标:掌握 readonly 修饰符和参数属性语法,能用更少的代码写出更安全的类。
上一章讲完访问修饰符,本章继续给类”加锁”——只读属性。另外还会介绍一个 TypeScript 独有的语法糖:参数属性,它能把三行代码压缩成一行。
readonly:只读属性
有些属性在对象创建后就不该再变了。比如用户 ID、票据的签发时间、订单创建时的快照数据。用 readonly 标记:
class User {
readonly id: number;
readonly createdAt: Date;
name: string;
constructor(id: number, name: string) {
this.id = id;
this.createdAt = new Date();
this.name = name;
}
}
const user = new User(1, "Alice");
console.log(user.id); // 1
// user.id = 2; // ❌ 不能修改 readonly 属性
user.name = "Bob"; // ✅ 普通属性可以修改
readonly 属性的赋值只能在两个地方:声明时和构造函数里。其他任何地方(包括类的方法内部)都不能改:
class Config {
readonly apiUrl = "https://api.example.com";
readonly timeout: number;
constructor(timeout: number) {
this.timeout = timeout; // ✅ 构造函数里可以赋值
}
updateTimeout(newVal: number) {
// this.timeout = newVal; // ❌ 方法内部不能改
}
}
readonly 是编译时约束
和 private 一样,readonly 只存在于 TypeScript 的类型检查阶段,编译成 JavaScript 后没有任何保护:
// 编译后的 JS 里,timeout 就是一个普通属性
// 在 JS 代码里随便改,TypeScript 管不着
如果你需要真正的不可变数据,得用 Object.freeze() 或者 Object.defineProperty 的 writable: false。
readonly 可以和其他修饰符组合
readonly 可以和 public、protected、private 任意组合:
class Person {
public readonly id: number; // 外部可读,不可写
protected readonly secret: string; // 子类可读,不可写
private readonly hash: string; // 仅内部可读,不可写
}
修饰符的顺序无所谓,readonly private 和 private readonly 效果一样。但社区惯例是 public readonly / protected readonly / private readonly。
readonly vs const
readonly 用在类属性上,const 用在变量上。两者都是”不可变”,但适用场景不同:
const MAX_SIZE = 100; // 全局常量用 const
class Box {
readonly maxSize = MAX_SIZE; // 类属性用 readonly
}
简单记:类属性用 readonly,普通变量用 const。
参数属性:三行变一行
来看一个写了很多遍的模式:
class Point {
x: number; // 声明
y: number; // 声明
constructor(x: number, y: number) {
this.x = x; // 赋值
this.y = y; // 赋值
}
}
声明 + 赋值的样板代码实在太啰嗦。TypeScript 提供了一个参数属性(parameter properties)语法,在构造函数参数前加修饰符,自动完成声明和赋值:
class Point {
constructor(public x: number, public y: number) {
// 构造函数体可以为空!
}
}
const p = new Point(3, 5);
console.log(p.x, p.y); // 3, 5
constructor(public x: number, public y: number) 这行代码等价于前面那六个步骤——声明了两个 public 属性,并在构造函数里把参数值赋给它们。
参数属性支持所有访问修饰符以及 readonly:
class User {
constructor(
public readonly id: number, // 公开只读
public name: string, // 公开可读写
protected role: string, // 保护
private token: string // 私有
) {
// 构造函数体可以加额外的初始化逻辑
console.log(`User ${name} created`);
}
getRole(): string {
return this.role; // ✅ 内部可以访问 protected
}
}
const user = new User(1, "Alice", "admin", "secret-token");
console.log(user.id); // 1
console.log(user.name); // "Alice"
// console.log(user.role); // ❌ protected 外部不可访问
// console.log(user.token); // ❌ private 外部不可访问
看看上面这段代码如果不用参数属性要写多少行:
// 不用参数属性:8 行样板代码
class User {
readonly id: number;
name: string;
protected role: string;
private token: string;
constructor(id: number, name: string, role: string, token: string) {
this.id = id;
this.name = name;
this.role = role;
this.token = token;
}
}
// 用参数属性:1 行搞定
class User {
constructor(
public readonly id: number,
public name: string,
protected role: string,
private token: string
) {}
}
Tip参数属性是 TypeScript 独有的语法,编译成 JavaScript 后会展开成普通的声明和赋值。它不影响运行时行为,只是帮你少写代码。
参数属性不能和普通构造函数参数混用吗?
可以混用。不需要变成属性的参数就不用加修饰符:
class Rect {
constructor(
public x: number, // 变成属性
public y: number, // 变成属性
private log: boolean // 变成私有属性
) {
if (log) {
console.log(`Rect created at (${x}, ${y})`);
}
}
}
参数属性的注意事项
- 参数属性声明后,不能再另外声明同名字段。 编译器会报重复声明。
- 参数属性默认值写在参数上,跟普通参数一样。
- 参数属性也受
strictPropertyInitialization检查。 不过因为参数属性在构造函数里自动赋值,不会触发这个错误。
实战组合
一个典型的应用场景:定义不可变的数据模型。
class OrderItem {
constructor(
public readonly productId: number,
public readonly productName: string,
public readonly quantity: number,
public readonly unitPrice: number
) {}
get totalPrice(): number {
return this.quantity * this.unitPrice;
}
}
const item = new OrderItem(101, "机械键盘", 2, 399);
console.log(item.totalPrice); // 798
// item.productId = 102; // ❌ readonly 不允许修改
这个类用参数属性声明了所有字段,加了 readonly 防止创建后被意外修改,用 getter 提供计算属性。整个类没有任何冗余代码。
小结
readonly让属性只能在声明时或构造函数里赋值,之后不可修改。readonly是编译时约束,运行时无保护。readonly可以和public、protected、private组合使用。- 参数属性
constructor(public x: number)是声明 + 赋值的简写,大幅减少样板代码。 - 参数属性支持所有修饰符,不影响运行时行为。
有了这三章的基础(类定义、修饰符、只读和参数属性),你已经可以写结构良好的独立类了。接下来要解决的是面向对象最核心的话题:继承。