Skip to content

私有字段

概述

私有字段(Private Fields)是 ES2022(ES2022)引入的 JavaScript 特性,使用 # 前缀语法来定义真正私有的类成员。与 TypeScript 的 private 修饰符不同,私有字段在运行时也是真正私有的,编译后的 JavaScript 代码中无法访问。私有字段提供了更强的封装性,确保类的内部实现细节不会被外部代码意外访问或修改。TypeScript 完全支持私有字段语法,并提供了完整的类型检查支持。

基本语法

定义私有字段

使用 # 前缀定义私有字段,私有字段必须在类中声明,不能通过 this 动态添加:

typescript
class BankAccount {
  // 私有字段:使用 # 前缀
  #balance: number;
  #accountNumber: string;
  
  // 公共属性
  public owner: string;
  
  constructor(owner: string, accountNumber: string, initialBalance: number = 0) {
    this.owner = owner;
    this.#accountNumber = accountNumber;
    this.#balance = initialBalance;
  }
  
  // 公共方法访问私有字段
  public deposit(amount: number): void {
    if (amount > 0) {
      this.#balance += amount;
      console.log(`Deposited ${amount}. New balance: ${this.#balance}`);
    }
  }
  
  public withdraw(amount: number): boolean {
    if (amount > 0 && amount <= this.#balance) {
      this.#balance -= amount;
      console.log(`Withdrew ${amount}. Remaining balance: ${this.#balance}`);
      return true;
    }
    return false;
  }
  
  public getBalance(): number {
    return this.#balance;  // 类内部可以访问私有字段
  }
  
  public getAccountNumber(): string {
    return this.#maskAccountNumber();  // 通过方法访问,不直接暴露
  }
  
  // 私有方法(使用 # 前缀)
  #maskAccountNumber(): string {
    return `****${this.#accountNumber.slice(-4)}`;
  }
}

const account = new BankAccount("Alice", "1234567890", 1000);
account.deposit(500);           // ✅ 可以调用公共方法
console.log(account.getBalance()); // ✅ 1500
console.log(account.getAccountNumber()); // ✅ "****7890"

// ❌ 错误:不能访问私有字段
// console.log(account.#balance);        // Error: Property '#balance' is not accessible outside class 'BankAccount'
// console.log(account.#accountNumber);  // Error: Property '#accountNumber' is not accessible outside class 'BankAccount'
// account.#maskAccountNumber();         // Error: Property '#maskAccountNumber' is not accessible outside class 'BankAccount'

私有字段的声明要求

私有字段必须在类体中声明,不能在构造函数或其他方法中动态添加:

typescript
class Example {
  // ✅ 正确:在类体中声明私有字段
  #value: string;
  
  constructor(value: string) {
    this.#value = value;  // ✅ 可以赋值
  }
  
  // ❌ 错误:不能在方法中动态添加私有字段
  // addPrivateField() {
  //   this.#newField = "value";  // Error: Property '#newField' does not exist
  // }
}

// ❌ 错误:不能在类外部添加私有字段
// const example = new Example("test");
// example.#newField = "value";  // Error: Property '#newField' does not exist

私有字段 vs private 修饰符

TypeScript 提供了两种私有化方式:private 修饰符和私有字段(#)。它们有重要的区别:

编译时私有 vs 运行时私有

private 修饰符:只在编译时检查,编译后的 JavaScript 中仍然可以访问:

typescript
class ExampleWithPrivate {
  private value: string = "secret";
  
  public getValue(): string {
    return this.value;
  }
}

const example = new ExampleWithPrivate();
// TypeScript 编译错误,但编译后的 JavaScript 可以访问
// console.log(example.value);  // TypeScript Error: Property 'value' is private

// 编译后的 JavaScript 代码类似:
// class ExampleWithPrivate {
//   constructor() {
//     this.value = "secret";  // 仍然是公共属性
//   }
// }
// 运行时可以访问:example.value

私有字段(#:在编译时和运行时都是私有的:

typescript
class ExampleWithHash {
  #value: string = "secret";
  
  public getValue(): string {
    return this.#value;
  }
}

const example = new ExampleWithHash();
// ❌ 编译时错误
// console.log(example.#value);  // Error: Property '#value' is not accessible

// ❌ 运行时也无法访问(真正的私有)
// 编译后的 JavaScript 使用 WeakMap 实现,外部无法访问

对比示例

typescript
// 使用 private 修饰符
class UserWithPrivate {
  private password: string;
  public username: string;
  
  constructor(username: string, password: string) {
    this.username = username;
    this.password = password;
  }
  
  public verifyPassword(input: string): boolean {
    return this.password === input;
  }
}

// 使用私有字段
class UserWithHash {
  #password: string;
  public username: string;
  
  constructor(username: string, password: string) {
    this.username = username;
    this.#password = password;
  }
  
  public verifyPassword(input: string): boolean {
    return this.#password === input;
  }
}

const user1 = new UserWithPrivate("alice", "secret123");
const user2 = new UserWithHash("bob", "secret456");

// 编译后的 JavaScript 中:
// user1.password 可能仍然可以访问(取决于编译目标)
// user2.#password 完全无法访问(真正的私有)

// TypeScript 中两者都不能直接访问
// console.log(user1.password);  // TypeScript Error
// console.log(user2.#password); // TypeScript Error

选择建议

特性private 修饰符私有字段(#
编译时检查
运行时私有
性能更好(直接属性访问)稍差(WeakMap 实现)
兼容性所有 TypeScript 版本需要 ES2022+
子类访问
推荐场景一般封装需求需要真正运行时私有

提示

  • 如果只需要编译时的类型安全,使用 private 修饰符即可
  • 如果需要真正的运行时私有性(如库开发、安全敏感场景),使用私有字段(#
  • 私有字段在编译后的代码中无法通过任何方式访问,提供了最强的封装性

使用示例

示例 1:用户认证系统

typescript
class User {
  #password: string;           // 私有字段:密码
  #salt: string;              // 私有字段:盐值
  public username: string;
  public email: string;
  
  constructor(username: string, email: string, password: string) {
    this.username = username;
    this.email = email;
    this.#salt = this.#generateSalt();
    this.#password = this.#hashPassword(password);
  }
  
  // 公共方法:验证密码
  public verifyPassword(inputPassword: string): boolean {
    const hashedInput = this.#hashPassword(inputPassword);
    return this.#password === hashedInput;
  }
  
  // 公共方法:更改密码
  public changePassword(oldPassword: string, newPassword: string): boolean {
    if (this.verifyPassword(oldPassword)) {
      this.#password = this.#hashPassword(newPassword);
      return true;
    }
    return false;
  }
  
  // 私有方法:生成盐值
  #generateSalt(): string {
    return Math.random().toString(36).substring(2, 15);
  }
  
  // 私有方法:哈希密码(简化示例)
  #hashPassword(password: string): string {
    // 实际应用中应使用专业的加密库
    return `${password}_${this.#salt}_hashed`;
  }
  
  // 公共方法:获取用户信息(不暴露敏感数据)
  public getPublicInfo(): { username: string; email: string } {
    return {
      username: this.username,
      email: this.email
    };
  }
}

const user = new User("alice", "alice@example.com", "myPassword123");
console.log(user.username);                    // ✅ "alice"
console.log(user.getPublicInfo());             // ✅ { username: "alice", email: "alice@example.com" }
console.log(user.verifyPassword("myPassword123")); // ✅ true
console.log(user.verifyPassword("wrong"));      // ✅ false

// ❌ 错误:无法访问私有字段
// console.log(user.#password);  // Error
// console.log(user.#salt);     // Error
// user.#hashPassword("test");  // Error

示例 2:缓存系统

typescript
class Cache {
  #cache: Map<string, { value: any; timestamp: number }>;
  #maxAge: number;  // 最大缓存时间(毫秒)
  
  constructor(maxAge: number = 60000) {  // 默认 1 分钟
    this.#cache = new Map();
    this.#maxAge = maxAge;
  }
  
  // 公共方法:设置缓存
  public set(key: string, value: any): void {
    this.#cache.set(key, {
      value,
      timestamp: Date.now()
    });
    this.#cleanExpired();  // 清理过期缓存
  }
  
  // 公共方法:获取缓存
  public get(key: string): any | null {
    const item = this.#cache.get(key);
    
    if (!item) {
      return null;
    }
    
    // 检查是否过期
    if (this.#isExpired(item.timestamp)) {
      this.#cache.delete(key);
      return null;
    }
    
    return item.value;
  }
  
  // 公共方法:清除所有缓存
  public clear(): void {
    this.#cache.clear();
  }
  
  // 公共方法:获取缓存统计
  public getStats(): { size: number; maxAge: number } {
    return {
      size: this.#cache.size,
      maxAge: this.#maxAge
    };
  }
  
  // 私有方法:检查是否过期
  #isExpired(timestamp: number): boolean {
    return Date.now() - timestamp > this.#maxAge;
  }
  
  // 私有方法:清理过期缓存
  #cleanExpired(): void {
    const now = Date.now();
    for (const [key, item] of this.#cache.entries()) {
      if (this.#isExpired(item.timestamp)) {
        this.#cache.delete(key);
      }
    }
  }
}

const cache = new Cache(5000);  // 5 秒过期
cache.set("user:1", { name: "Alice", age: 30 });
cache.set("user:2", { name: "Bob", age: 25 });

console.log(cache.get("user:1"));  // ✅ { name: "Alice", age: 30 }
console.log(cache.getStats());      // ✅ { size: 2, maxAge: 5000 }

// 等待 6 秒后
setTimeout(() => {
  console.log(cache.get("user:1"));  // ✅ null(已过期)
  console.log(cache.getStats());      // ✅ { size: 0, maxAge: 5000 }
}, 6000);

// ❌ 错误:无法直接访问内部缓存
// console.log(cache.#cache);  // Error
// cache.#cleanExpired();      // Error

示例 3:计数器类(防止外部修改)

typescript
class Counter {
  #count: number = 0;  // 私有字段:防止外部直接修改
  #step: number;        // 私有字段:步长
  
  constructor(initialValue: number = 0, step: number = 1) {
    this.#count = initialValue;
    this.#step = step;
  }
  
  // 公共方法:增加
  public increment(): number {
    this.#count += this.#step;
    return this.#count;
  }
  
  // 公共方法:减少
  public decrement(): number {
    this.#count -= this.#step;
    return this.#count;
  }
  
  // 公共方法:重置
  public reset(): void {
    this.#count = 0;
  }
  
  // 公共方法:获取当前值
  public getValue(): number {
    return this.#count;
  }
  
  // 公共方法:设置步长
  public setStep(step: number): void {
    if (step > 0) {
      this.#step = step;
    }
  }
  
  // 公共方法:获取步长
  public getStep(): number {
    return this.#step;
  }
}

const counter = new Counter(10, 2);
console.log(counter.getValue());  // ✅ 10
counter.increment();              // ✅ 12
counter.increment();              // ✅ 14
counter.decrement();              // ✅ 12
console.log(counter.getStep());   // ✅ 2

// ❌ 错误:无法直接修改私有字段
// counter.#count = 100;  // Error: Property '#count' is not accessible
// counter.#step = 5;     // Error: Property '#step' is not accessible

// ✅ 只能通过公共方法修改
counter.setStep(3);
counter.increment();  // ✅ 15(12 + 3)

私有字段与继承

私有字段在子类中也是不可访问的,这与 private 修饰符的行为一致:

typescript
class Base {
  #privateField: string = "base private";
  private privateModifier: string = "base private modifier";
  protected protectedField: string = "base protected";
  
  public getPrivateField(): string {
    return this.#privateField;  // ✅ 类内部可以访问
  }
  
  public getPrivateModifier(): string {
    return this.privateModifier;  // ✅ 类内部可以访问
  }
}

class Derived extends Base {
  #derivedPrivate: string = "derived private";
  
  public testAccess(): void {
    // ✅ 可以访问 protected
    console.log(this.protectedField);
    
    // ❌ 错误:不能访问父类的私有字段
    // console.log(this.#privateField);  // Error: Property '#privateField' is not accessible
    
    // ❌ 错误:不能访问父类的 private 修饰符成员
    // console.log(this.privateModifier);  // Error: Property 'privateModifier' is private
    
    // ✅ 可以通过公共方法访问
    console.log(this.getPrivateField());  // ✅ "base private"
    console.log(this.getPrivateModifier()); // ✅ "base private modifier"
    
    // ✅ 可以访问自己的私有字段
    console.log(this.#derivedPrivate);  // ✅ "derived private"
  }
}

const derived = new Derived();
derived.testAccess();

// ❌ 错误:外部无法访问任何私有成员
// console.log(derived.#privateField);     // Error
// console.log(derived.#derivedPrivate);   // Error
// console.log(derived.privateModifier);   // Error

私有字段与静态成员

私有字段也可以是静态的,使用 static # 语法:

typescript
class Logger {
  static #logs: string[] = [];  // 静态私有字段
  static #maxLogs: number = 100;
  
  // 静态公共方法
  static log(message: string): void {
    const timestamp = new Date().toISOString();
    const logEntry = `[${timestamp}] ${message}`;
    
    Logger.#logs.push(logEntry);
    
    // 限制日志数量
    if (Logger.#logs.length > Logger.#maxLogs) {
      Logger.#logs.shift();  // 移除最旧的日志
    }
    
    console.log(logEntry);
  }
  
  // 静态公共方法:获取日志
  static getLogs(): string[] {
    return [...Logger.#logs];  // 返回副本,防止外部修改
  }
  
  // 静态公共方法:清除日志
  static clearLogs(): void {
    Logger.#logs = [];
  }
  
  // 静态公共方法:设置最大日志数
  static setMaxLogs(max: number): void {
    if (max > 0) {
      Logger.#maxLogs = max;
    }
  }
  
  // 静态私有方法
  static #formatLog(message: string): string {
    return `[${new Date().toISOString()}] ${message}`;
  }
}

Logger.log("Application started");
Logger.log("User logged in");
Logger.log("Data saved");

console.log(Logger.getLogs());  // ✅ 获取所有日志
Logger.clearLogs();             // ✅ 清除日志

// ❌ 错误:无法访问静态私有字段
// console.log(Logger.#logs);      // Error
// Logger.#formatLog("test");      // Error

类型检查示例

常见错误

typescript
// ❌ 错误:外部访问私有字段
class Example {
  #value: string = "secret";
}

const example = new Example();
console.log(example.#value);
// Error: Property '#value' is not accessible outside class 'Example'

// ❌ 错误:未声明的私有字段
class Example2 {
  #value: string = "value";
  
  setValue() {
    this.#newValue = "new";  // Error: Property '#newValue' does not exist
  }
}

// ❌ 错误:子类访问父类私有字段
class Parent {
  #parentPrivate: string = "parent";
}

class Child extends Parent {
  getParentPrivate(): string {
    return this.#parentPrivate;  // Error: Property '#parentPrivate' is not accessible
  }
}

// ❌ 错误:在类外部声明私有字段
// const obj = {};
// obj.#field = "value";  // Error: Private fields can only be declared in a class

正确写法

typescript
// ✅ 正确:通过公共方法访问私有字段
class Example {
  #value: string = "secret";
  
  public getValue(): string {
    return this.#value;
  }
  
  public setValue(value: string): void {
    this.#value = value;
  }
}

const example = new Example();
console.log(example.getValue());  // ✅ "secret"
example.setValue("new secret");
console.log(example.getValue());  // ✅ "new secret"

// ✅ 正确:在类中正确声明和使用私有字段
class Counter {
  #count: number = 0;
  
  increment(): void {
    this.#count++;  // ✅ 类内部可以访问
  }
  
  getCount(): number {
    return this.#count;  // ✅ 类内部可以访问
  }
}

// ✅ 正确:通过公共方法让子类间接访问
class Base {
  #value: string = "base";
  
  protected getValue(): string {
    return this.#value;  // ✅ 通过受保护方法暴露
  }
}

class Derived extends Base {
  public showValue(): string {
    return this.getValue();  // ✅ 通过继承的方法访问
  }
}

注意事项

提示

  • 私有字段使用 # 前缀,必须在类体中声明
  • 私有字段在运行时也是真正私有的,提供了最强的封装性
  • 私有字段可以是实例字段或静态字段
  • 私有字段也支持方法(私有方法)
  • 私有字段在编译后的代码中无法通过任何方式访问

注意

  • 私有字段需要 ES2022+ 支持,确保编译目标正确配置
  • 私有字段在子类中不可访问,如果需要子类访问,使用 protected 修饰符
  • 私有字段不能通过 this 动态添加,必须在类体中声明
  • 私有字段的性能略低于普通属性(使用 WeakMap 实现)
  • 私有字段与 private 修饰符可以同时使用,但通常选择一种即可

重要

  • 运行时私有性:私有字段是唯一提供真正运行时私有的方式
  • 安全性:对于安全敏感的代码(如密码、密钥),优先使用私有字段
  • 库开发:开发供他人使用的库时,私有字段可以防止外部代码访问内部实现
  • 性能考虑:如果性能敏感且不需要运行时私有,可以使用 private 修饰符
  • 兼容性:确保目标环境支持 ES2022+,或使用适当的转译工具

信息

何时使用私有字段(#

  1. 需要运行时私有性

    • 开发库或框架
    • 安全敏感的应用
    • 需要防止外部代码访问内部实现
  2. 真正的封装需求

    • 不希望任何外部代码(包括运行时)访问
    • 需要最强的封装保证

何时使用 private 修饰符

  1. 只需要编译时类型安全

    • 一般应用开发
    • 性能敏感的场景
    • 不需要运行时私有性
  2. 兼容性要求

    • 需要支持较旧的 JavaScript 版本
    • 不需要 ES2022+ 特性

最佳实践

  • 对于新项目,如果目标环境支持,优先考虑私有字段
  • 对于安全敏感的数据,必须使用私有字段
  • 对于一般封装需求,private 修饰符通常足够

相关链接

基于 VitePress 构建