Skip to content

只读属性

概述

只读属性(Readonly Properties)是 TypeScript 类中的一个重要特性,使用 readonly 关键字可以将类的属性标记为只读。只读属性只能在声明时或构造函数中初始化,之后不能被修改。这个特性有助于创建不可变的数据结构,提高代码的安全性和可维护性。

基本语法

声明只读属性

使用 readonly 关键字在类中声明只读属性:

typescript
class User {
  readonly id: number;
  name: string;
  readonly createdAt: Date;
  
  constructor(id: number, name: string) {
    this.id = id;  // 可以在构造函数中初始化
    this.name = name;
    this.createdAt = new Date();  // 可以在构造函数中初始化
  }
}

const user = new User(1, "Alice");
console.log(user.id);  // 1
console.log(user.createdAt);  // 当前日期

// ❌ 错误:不能修改只读属性
// user.id = 2; // Error: Cannot assign to 'id' because it is a read-only property
// user.createdAt = new Date(); // Error

声明时初始化

只读属性也可以在声明时直接初始化:

typescript
class Config {
  readonly apiKey: string = "default-key";
  readonly version: string = "1.0.0";
  timeout: number = 5000;  // 普通属性
  
  constructor(apiKey?: string) {
    if (apiKey) {
      this.apiKey = apiKey;  // 可以在构造函数中重新赋值
    }
  }
}

const config = new Config("custom-key");
console.log(config.apiKey);  // "custom-key"
console.log(config.version); // "1.0.0"

// ❌ 错误:不能在构造函数外修改
// config.apiKey = "new-key"; // Error

提示

只读属性可以在两个地方初始化:

  1. 声明时直接赋值
  2. 构造函数中赋值

一旦对象创建完成,只读属性就不能再被修改。

与访问修饰符组合使用

只读属性可以与访问修饰符(publicprivateprotected)组合使用:

public readonly(默认)

public readonly 是默认的访问级别,属性可以在外部读取但不能修改:

typescript
class Product {
  public readonly id: number;
  public readonly sku: string;
  public name: string;  // 可读可写
  
  constructor(id: number, sku: string, name: string) {
    this.id = id;
    this.sku = sku;
    this.name = name;
  }
}

const product = new Product(1, "SKU-001", "Laptop");
console.log(product.id);   // ✅ 可以读取
console.log(product.sku);   // ✅ 可以读取
console.log(product.name);  // ✅ 可以读取

product.name = "Desktop";   // ✅ 可以修改普通属性
// product.id = 2;          // ❌ 错误:不能修改只读属性
// product.sku = "SKU-002"; // ❌ 错误:不能修改只读属性

private readonly

private readonly 属性只能在类内部访问,且不能被修改:

typescript
class BankAccount {
  private readonly accountNumber: string;
  private balance: number;
  
  constructor(accountNumber: string, initialBalance: number) {
    this.accountNumber = accountNumber;
    this.balance = initialBalance;
  }
  
  public getAccountNumber(): string {
    return this.accountNumber;  // ✅ 类内部可以访问
  }
  
  public deposit(amount: number): void {
    this.balance += amount;
    // this.accountNumber = "new-number"; // ❌ 错误:不能修改只读属性
  }
}

const account = new BankAccount("ACC-12345", 1000);
console.log(account.getAccountNumber()); // "ACC-12345"

// ❌ 错误:外部不能访问 private 属性
// console.log(account.accountNumber); // Error: Property 'accountNumber' is private

protected readonly

protected readonly 属性可以在类内部和子类中访问,但不能修改:

typescript
class Animal {
  protected readonly species: string;
  protected name: string;
  
  constructor(species: string, name: string) {
    this.species = species;
    this.name = name;
  }
  
  public getInfo(): string {
    return `${this.name} is a ${this.species}`;
  }
}

class Dog extends Animal {
  constructor(name: string) {
    super("Canine", name);
  }
  
  public bark(): void {
    console.log(`${this.name} (${this.species}) barks!`);  // ✅ 子类可以访问
    // this.species = "Feline"; // ❌ 错误:不能修改只读属性
  }
}

const dog = new Dog("Buddy");
dog.bark(); // "Buddy (Canine) barks!"

使用场景

场景 1:唯一标识符

只读属性非常适合用于唯一标识符,如用户 ID、订单号等:

typescript
class Order {
  readonly orderId: string;
  readonly customerId: number;
  status: string;
  total: number;
  
  constructor(orderId: string, customerId: number, total: number) {
    this.orderId = orderId;
    this.customerId = customerId;
    this.total = total;
    this.status = "pending";
  }
  
  public updateStatus(newStatus: string): void {
    this.status = newStatus;  // ✅ 可以修改状态
    // this.orderId = "new-id"; // ❌ 错误:订单 ID 不应该改变
  }
  
  public getOrderInfo(): string {
    return `Order ${this.orderId} for customer ${this.customerId}: ${this.status}`;
  }
}

const order = new Order("ORD-001", 123, 99.99);
console.log(order.getOrderInfo()); // "Order ORD-001 for customer 123: pending"

order.updateStatus("completed");
console.log(order.getOrderInfo()); // "Order ORD-001 for customer 123: completed"

场景 2:时间戳

创建时间、更新时间等时间戳属性应该使用只读:

typescript
class Document {
  readonly id: number;
  readonly createdAt: Date;
  title: string;
  content: string;
  updatedAt: Date;
  
  constructor(id: number, title: string, content: string) {
    this.id = id;
    this.createdAt = new Date();  // 创建时间在对象创建时确定
    this.updatedAt = new Date();
    this.title = title;
    this.content = content;
  }
  
  public updateContent(newContent: string): void {
    this.content = newContent;
    this.updatedAt = new Date();  // ✅ 可以更新修改时间
    // this.createdAt = new Date(); // ❌ 错误:创建时间不应该改变
  }
  
  public getAge(): number {
    const now = new Date();
    return now.getTime() - this.createdAt.getTime();
  }
}

const doc = new Document(1, "My Document", "Initial content");
console.log(`Document created at: ${doc.createdAt}`);

doc.updateContent("Updated content");
console.log(`Last updated at: ${doc.updatedAt}`);

场景 3:配置对象

配置对象中的固定配置项应该使用只读:

typescript
class DatabaseConnection {
  readonly host: string;
  readonly port: number;
  readonly database: string;
  private connected: boolean = false;
  
  constructor(host: string, port: number, database: string) {
    this.host = host;
    this.port = port;
    this.database = database;
  }
  
  public connect(): void {
    if (!this.connected) {
      console.log(`Connecting to ${this.host}:${this.port}/${this.database}`);
      this.connected = true;
    }
  }
  
  public getConnectionString(): string {
    return `${this.host}:${this.port}/${this.database}`;
  }
}

const db = new DatabaseConnection("localhost", 5432, "mydb");
db.connect(); // "Connecting to localhost:5432/mydb"

// ❌ 错误:连接配置不应该改变
// db.host = "remote-host"; // Error

场景 4:常量值

类中的常量值应该使用 readonlystatic 组合:

typescript
class MathUtils {
  static readonly PI: number = 3.14159;
  static readonly E: number = 2.71828;
  static readonly MAX_SAFE_INTEGER: number = Number.MAX_SAFE_INTEGER;
  
  static circleArea(radius: number): number {
    return MathUtils.PI * radius * radius;
  }
}

console.log(MathUtils.PI);  // 3.14159
console.log(MathUtils.circleArea(5)); // 78.53975

// ❌ 错误:不能修改静态只读属性
// MathUtils.PI = 3.14; // Error

只读属性与接口

只读属性在接口中也有类似的概念,但行为略有不同:

接口中的只读属性

typescript
interface User {
  readonly id: number;
  name: string;
}

const user: User = {
  id: 1,
  name: "Alice"
};

// ❌ 错误:不能修改接口中的只读属性
// user.id = 2; // Error

类实现接口

类实现包含只读属性的接口时,需要使用 readonly 关键字:

typescript
interface Identifiable {
  readonly id: number;
  name: string;
}

class User implements Identifiable {
  readonly id: number;  // 必须使用 readonly
  name: string;
  
  constructor(id: number, name: string) {
    this.id = id;
    this.name = name;
  }
}

const user = new User(1, "Alice");
// user.id = 2; // ❌ 错误

只读数组和对象

只读数组属性

如果属性是数组类型,readonly 只防止重新赋值,不防止修改数组内容:

typescript
class ShoppingCart {
  readonly items: string[];  // 只读数组引用
  
  constructor(items: string[] = []) {
    this.items = items;
  }
  
  public addItem(item: string): void {
    this.items.push(item);  // ✅ 可以修改数组内容
  }
  
  public removeItem(item: string): void {
    const index = this.items.indexOf(item);
    if (index > -1) {
      this.items.splice(index, 1);  // ✅ 可以修改数组内容
    }
  }
  
  // ❌ 错误:不能重新赋值整个数组
  // public reset(): void {
  //   this.items = []; // Error: Cannot assign to 'items' because it is a read-only property
  // }
}

const cart = new ShoppingCart(["Apple", "Banana"]);
cart.addItem("Orange");
console.log(cart.items); // ["Apple", "Banana", "Orange"]

// ❌ 错误:不能重新赋值
// cart.items = []; // Error

使用 ReadonlyArray

如果需要真正的只读数组,可以使用 ReadonlyArray 类型:

typescript
class ReadOnlyList {
  readonly items: ReadonlyArray<string>;  // 只读数组类型
  
  constructor(items: string[]) {
    this.items = items;
  }
  
  public getItem(index: number): string | undefined {
    return this.items[index];  // ✅ 可以读取
  }
  
  // ❌ 错误:不能修改只读数组
  // public addItem(item: string): void {
  //   this.items.push(item); // Error: Property 'push' does not exist on type 'ReadonlyArray<string>'
  // }
}

const list = new ReadOnlyList(["A", "B", "C"]);
console.log(list.getItem(0)); // "A"

只读对象属性

对于对象类型的属性,readonly 只防止重新赋值,不防止修改对象内容:

typescript
class UserProfile {
  readonly metadata: { version: number; created: Date };
  
  constructor() {
    this.metadata = {
      version: 1,
      created: new Date()
    };
  }
  
  public updateVersion(): void {
    this.metadata.version = 2;  // ✅ 可以修改对象内容
    // this.metadata = { version: 2, created: new Date() }; // ❌ 错误:不能重新赋值
  }
}

const profile = new UserProfile();
profile.updateVersion();
console.log(profile.metadata.version); // 2

类型检查示例

常见错误

typescript
// ❌ 错误:在构造函数外修改只读属性
class User {
  readonly id: number;
  
  constructor(id: number) {
    this.id = id;
  }
  
  public changeId(newId: number): void {
    // this.id = newId; // Error: Cannot assign to 'id' because it is a read-only property
  }
}

const user = new User(1);
// user.id = 2; // Error: Cannot assign to 'id' because it is a read-only property

// ❌ 错误:未初始化的只读属性
class Config {
  readonly apiKey: string;  // 错误:必须初始化
  
  constructor() {
    // 如果没有在构造函数中初始化,会报错
  }
}
// Error: Property 'apiKey' has no initializer and is not definitely assigned in the constructor

正确写法

typescript
// ✅ 正确:在构造函数中初始化
class User {
  readonly id: number;
  name: string;
  
  constructor(id: number, name: string) {
    this.id = id;
    this.name = name;
  }
}

const user = new User(1, "Alice");
console.log(user.id); // 1

// ✅ 正确:声明时初始化
class Config {
  readonly apiKey: string = "default-key";
  readonly version: string = "1.0.0";
}

const config = new Config();
console.log(config.apiKey); // "default-key"

// ✅ 正确:使用访问修饰符组合
class Product {
  public readonly id: number;
  private readonly sku: string;
  
  constructor(id: number, sku: string) {
    this.id = id;
    this.sku = sku;
  }
  
  public getSku(): string {
    return this.sku;  // 类内部可以访问
  }
}

const product = new Product(1, "SKU-001");
console.log(product.id);    // ✅ 可以访问
console.log(product.getSku()); // ✅ 通过方法访问

注意事项

提示

  • 只读属性适合用于不会改变的值,如 ID、创建时间、配置项等
  • 只读属性可以在声明时或构造函数中初始化
  • 只读属性可以与访问修饰符(public、private、protected)组合使用
  • 对于数组和对象类型的只读属性,readonly 只防止重新赋值,不防止修改内容
  • 如果需要真正的不可变数组,使用 ReadonlyArray<T> 类型

注意

  • readonly 只在编译时生效,运行时仍然可以修改。如果需要真正的不可变对象,可以使用 Object.freeze() 或第三方库(如 Immutable.js)
  • 只读属性必须在声明时或构造函数中初始化,否则会报错
  • 只读属性不能有 setter,但可以有 getter
  • 对于数组和对象,readonly 只保护引用不被重新赋值,不保护内容不被修改

重要

  • 只读属性是 TypeScript 编译时的特性,编译后的 JavaScript 代码中不会保留 readonly 信息
  • 如果需要运行时保护,必须使用 Object.freeze() 或其他运行时机制
  • 只读属性与 const 关键字不同:const 用于变量,readonly 用于类属性

信息

只读属性的优势

  • 类型安全:编译时防止意外修改
  • 代码清晰:明确标识不应该改变的值
  • 维护性:减少因意外修改导致的 bug

适用场景

  • 唯一标识符(ID、订单号等)
  • 时间戳(创建时间等)
  • 配置项(连接信息、常量等)
  • 不可变数据结构

相关链接

基于 VitePress 构建