JS `private` 字段 (`#`) 的实际应用:实现严格封装的类

各位朋友,大家好!今天咱们来聊聊 JavaScript 中 private 字段(#),这玩意儿听起来挺高大上,但实际上用好了,能让你的代码更安全,更可靠,也更容易维护。咱们的目标就是:彻底搞懂它,并且能熟练地运用到实际开发中。

开场白:为什么需要“私有”?

想象一下,你是一个玩具设计师,设计了一个非常酷炫的遥控车。遥控车里面有很多精密的齿轮、电路板,还有一些非常重要的参数,比如电池电量、电机转速等等。

如果你允许小朋友们随便拆开遥控车,随便调整里面的参数,那会发生什么?

  • 遥控车坏掉: 小朋友可能会把齿轮搞错位,或者烧坏电路板。
  • 遥控车行为异常: 电池电量被随意修改,可能导致遥控车“假死”;电机转速被调得过高,可能会烧毁电机。

所以,玩具设计师需要一种方法,把遥控车内部的关键部件和参数“藏起来”,只允许通过特定的接口(比如遥控器)来控制遥控车。

在编程世界里,也一样。我们需要一种方法,把类的内部状态和行为“藏起来”,防止外部代码随意修改,从而保证类的稳定性和可靠性。这就是“封装”的思想。

“私有”的历史:JavaScript 的“伪私有”时代

private 字段(#)出现之前,JavaScript 实现“私有”的方式可谓是八仙过海,各显神通。

  • 命名约定(___): 这是一种最简单的“约定式私有”。我们在变量或方法名前面加上 ___,表示这是私有的,不应该在外部直接访问。

    class Car {
      constructor() {
        this._speed = 0; // 用 _speed 表示私有属性
      }
    
      accelerate() {
        this._speed += 10;
      }
    
      getSpeed() {
        return this._speed;
      }
    }
    
    const myCar = new Car();
    myCar._speed = 1000; // 仍然可以访问,只是不推荐
    console.log(myCar.getSpeed()); // 输出 1000

    问题: 这只是一个“君子协定”,并没有真正的约束力。外部代码仍然可以随意访问和修改 _speed

  • 闭包(Closure): 利用闭包的特性,将变量或方法包裹在一个函数内部,外部无法直接访问。

    const createCar = () => {
      let speed = 0; // speed 变量被闭包保护
    
      return {
        accelerate() {
          speed += 10;
        },
        getSpeed() {
          return speed;
        }
      };
    };
    
    const myCar = createCar();
    // console.log(myCar.speed); // 无法访问 speed
    myCar.accelerate();
    console.log(myCar.getSpeed()); // 输出 10

    问题: 这种方式可以实现真正的私有,但是每个实例都会创建一个新的闭包,导致内存占用增加,并且无法使用 prototype 共享方法。

  • WeakMap: 使用 WeakMap 来存储私有变量,将实例作为键,私有变量作为值。

    const carSpeeds = new WeakMap();
    
    class Car {
      constructor() {
        carSpeeds.set(this, 0); // 将实例和 speed 关联
      }
    
      accelerate() {
        carSpeeds.set(this, carSpeeds.get(this) + 10);
      }
    
      getSpeed() {
        return carSpeeds.get(this);
      }
    }
    
    const myCar = new Car();
    // console.log(carSpeeds.get(myCar)); // 无法直接访问 WeakMap
    myCar.accelerate();
    console.log(myCar.getSpeed()); // 输出 10

    问题: 这种方式相对比较复杂,代码可读性较差。

private 字段(#):真正的私有!

终于,JavaScript 引入了 private 字段(#),这是一种真正的、强制的私有机制。

class Car {
  #speed = 0; // 使用 # 定义私有字段

  accelerate() {
    this.#speed += 10;
  }

  getSpeed() {
    return this.#speed;
  }
}

const myCar = new Car();
// console.log(myCar.#speed); // 报错:私有字段 '#speed' 必须在封闭类中声明
myCar.accelerate();
console.log(myCar.getSpeed()); // 输出 10

class ElectricCar extends Car{
    increaseSpeed(){
        //this.#speed += 20; //报错: 私有字段 '#speed' 必须在封闭类中声明
    }
}

关键点:

  • # 符号: 在字段名前面加上 # 符号,表示这是一个私有字段。
  • 封闭类: 私有字段只能在声明它的类中访问。这意味着子类也无法访问父类的私有字段。
  • 编译时错误: 如果尝试在类外部访问私有字段,或者在子类中访问父类的私有字段,会直接抛出 SyntaxError 错误。

private 字段的优点

  • 真正的私有性: 强制性的私有,杜绝了外部代码随意修改内部状态的可能。
  • 代码清晰: 使用 # 符号,清晰地标识了哪些字段是私有的,提高了代码可读性。
  • 安全性: 防止恶意代码篡改内部状态,提高了代码的安全性。
  • 可维护性: 可以放心地修改类的内部实现,而不用担心影响到外部代码。

private 字段的局限性

  • 子类无法访问父类的私有字段: 这可能会限制代码的复用性。
  • 无法在构造函数之外声明私有字段: 必须在类的主体中声明私有字段。

private 字段的应用场景

  • 封装敏感数据: 例如,密码、密钥等。
  • 保护内部状态: 防止外部代码随意修改类的内部状态,保证类的稳定性和可靠性。
  • 隐藏实现细节: 将类的内部实现细节隐藏起来,只暴露必要的接口给外部使用。

高级用法:private 字段与方法

private 不仅仅可以用于字段,还可以用于方法!

class Counter {
  #count = 0;

  #increment() { // 私有方法
    this.#count++;
  }

  incrementPublicly() {
    this.#increment();
  }

  getCount() {
    return this.#count;
  }
}

const myCounter = new Counter();
myCounter.incrementPublicly();
console.log(myCounter.getCount()); // 输出 1
// myCounter.#increment(); // 报错:私有方法 '#increment' 必须在封闭类中声明

private 字段与静态字段/方法

private 字段也可以与 static 关键字一起使用,创建私有静态字段和方法。

class MyClass {
  static #counter = 0;

  static #increment() {
    MyClass.#counter++;
  }

  static getCounter() {
    MyClass.#increment();
    return MyClass.#counter;
  }
}

console.log(MyClass.getCounter()); // 输出 1
// console.log(MyClass.#counter); // 报错:私有字段 '#counter' 必须在封闭类中声明

private 字段与 getter/setter

虽然不能直接访问 private 字段,但是可以通过 gettersetter 间接访问和修改。

class Person {
  #age = 0;

  get age() {
    return this.#age;
  }

  set age(newAge) {
    if (newAge >= 0 && newAge <= 150) {
      this.#age = newAge;
    } else {
      console.warn("Invalid age!");
    }
  }
}

const person = new Person();
person.age = 30;
console.log(person.age); // 输出 30
person.age = -10; // 输出 "Invalid age!"

实际案例:模拟银行账户

让我们用 private 字段来模拟一个银行账户,保护账户余额。

class BankAccount {
  #balance = 0;

  constructor(initialBalance) {
    if (initialBalance > 0) {
      this.#balance = initialBalance;
    } else {
      console.error("Initial balance must be positive.");
    }
  }

  deposit(amount) {
    if (amount > 0) {
      this.#balance += amount;
    } else {
      console.error("Deposit amount must be positive.");
    }
  }

  withdraw(amount) {
    if (amount > 0 && amount <= this.#balance) {
      this.#balance -= amount;
    } else {
      console.error("Invalid withdrawal amount.");
    }
  }

  getBalance() {
    return this.#balance;
  }
}

const myAccount = new BankAccount(1000);
myAccount.deposit(500);
myAccount.withdraw(200);
console.log(myAccount.getBalance()); // 输出 1300
// myAccount.#balance = -10000; // 报错:私有字段 '#balance' 必须在封闭类中声明

总结:private 字段,你值得拥有!

private 字段是 JavaScript 中一个非常重要的特性,它提供了真正的、强制的私有机制,可以帮助我们编写更安全、更可靠、更容易维护的代码。虽然它有一些局限性,但是只要合理运用,就能发挥出巨大的作用。

表格:private 字段 vs. 其他“伪私有”方式

特性 命名约定(_ 闭包 WeakMap private 字段(#
私有性 约定式 真正私有 真正私有 真正私有
强制性
性能 较低 中等
可读性 较高 较低 较低 较高
内存占用 较高 中等
易用性 中等 中等
子类可访问

最后,给大家留个思考题:

如果我们需要实现一个可复用的组件,并且希望子类能够访问父类的一些内部状态,但是又不希望外部代码直接访问,应该怎么做?(提示:可以考虑使用 protected 概念,或者使用 Symbol 作为键来存储“受保护”的属性。)

希望今天的讲解对大家有所帮助。谢谢大家!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注