JS `Private Methods` 与 `Private Accessors` (ES2022):类的封装性增强

大家好,我是今天的主讲人,咱们聊聊 JavaScript 里那些“藏起来的小秘密”——也就是 ES2022 引入的 Private MethodsPrivate Accessors。它们可是提升类封装性的利器!准备好了吗?咱们开始!

开场白:封装,封装,还是封装!

在面向对象编程的世界里,封装绝对是核心概念之一。它就像给你的代码穿上盔甲,保护内部数据不被外部随意篡改,让代码更加健壮,更容易维护。

想象一下,你有一辆汽车。你可以开车、加油、换挡,但你不能直接控制发动机内部的每一个零件。那是因为汽车制造商对发动机进行了封装,只暴露了必要的接口给你。

JavaScript 之前的版本,虽然也能模拟私有属性和方法,但总感觉差点意思。ES2022 带来的 Private MethodsPrivate Accessors,才是真正的“原生”私有,让封装变得更加可靠。

第一幕:老朋友的“伪装术”——之前的私有模拟

在 ES2022 之前,JavaScript 社区流行几种模拟私有的方法,但它们都有各自的局限性。

  • 命名约定(约定成俗,但防君子不防小人)

    最常见的就是使用下划线 _ 开头来表示私有属性或方法。

    class Counter {
      constructor() {
        this._count = 0; // 下划线表示私有,但外部仍然可以访问
      }
    
      _increment() { // 下划线表示私有,但外部仍然可以调用
        this._count++;
      }
    
      getCount() {
        return this._count;
      }
    }
    
    const counter = new Counter();
    counter._count = 100; // 仍然可以修改!
    console.log(counter.getCount()); // 输出 100

    这种方式完全依赖开发者的自觉性,只能算是一种“君子协定”。只要你想,随时都可以访问和修改。

  • 闭包(稍微靠谱,但略显笨重)

    利用闭包的特性,将私有变量和方法放在函数内部,然后返回一个包含公有方法的对象。

    function createCounter() {
      let count = 0; // 私有变量
    
      return {
        increment() {
          count++;
        },
        getCount() {
          return count;
        }
      };
    }
    
    const counter = createCounter();
    counter.count = 100; // 无法修改私有变量 count
    counter.increment();
    console.log(counter.getCount()); // 输出 1

    这种方式确实可以实现私有,但代码结构会变得复杂,而且每次创建实例都要创建一个新的闭包,略显笨重。

  • WeakMap(性能尚可,但语法稍显繁琐)

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

    const _count = new WeakMap();
    
    class Counter {
      constructor() {
        _count.set(this, 0);
      }
    
      increment() {
        _count.set(this, _count.get(this) + 1);
      }
    
      getCount() {
        return _count.get(this);
      }
    }
    
    const counter = new Counter();
    // 无法直接访问私有变量
    counter.increment();
    console.log(counter.getCount()); // 输出 1

    WeakMap 相对闭包性能更好,但语法也比较繁琐,需要单独创建一个 WeakMap 对象,并在每次访问私有变量时都要使用 getset 方法。

第二幕:ES2022 的“真·私有”登场!

ES2022 引入了使用 # 前缀来声明私有属性和方法。这是一种真正的、原生的私有,JavaScript 引擎会强制限制外部访问。

  • Private Fields(私有字段)

    class Counter {
      #count = 0; // 私有字段
    
      increment() {
        this.#count++;
      }
    
      getCount() {
        return this.#count;
      }
    }
    
    const counter = new Counter();
    //console.log(counter.#count); // 报错:Private field '#count' must be declared in an enclosing class
    counter.increment();
    console.log(counter.getCount()); // 输出 1
    
    class SubCounter extends Counter {
      // incrementSub() {
      //   this.#count++; // 报错:Private field '#count' must be declared in an enclosing class
      // }
    }

    注意几点:

    1. 私有字段必须在类中声明,不能在构造函数外部动态添加。
    2. 只能在类内部访问私有字段,包括方法和访问器。
    3. 子类无法访问父类的私有字段。这是为了保证封装的彻底性。
  • Private Methods(私有方法)

    class Calculator {
      #add(x, y) { // 私有方法
        return x + y;
      }
    
      calculate(x, y) {
        return this.#add(x, y);
      }
    }
    
    const calculator = new Calculator();
    //console.log(calculator.#add(1, 2)); // 报错:Private field '#add' must be declared in an enclosing class
    console.log(calculator.calculate(1, 2)); // 输出 3

    私有方法只能在类内部调用,外部无法访问。

  • Private Accessors(私有访问器)

    私有访问器允许你控制对私有字段的读取和设置,就像 gettersetter 一样,只不过是私有的。

    class Temperature {
      #celsius = 0;
    
      get #fahrenheit() { // 私有 getter
        return (this.#celsius * 9) / 5 + 32;
      }
    
      set #fahrenheit(value) { // 私有 setter
        this.#celsius = ((value - 32) * 5) / 9;
      }
    
      getCelsius() {
        return this.#celsius;
      }
    
      setFahrenheit(value) {
        this.#fahrenheit = value; // 通过私有 setter 设置摄氏度
      }
    
      getFahrenheit() {
        return this.#fahrenheit; // 通过私有 getter 获取华氏度
      }
    }
    
    const temp = new Temperature();
    temp.setFahrenheit(68);
    console.log(temp.getCelsius()); // 输出 20
    console.log(temp.getFahrenheit()); // 输出 68

    私有访问器可以让你在读取和设置私有字段时执行一些额外的逻辑,比如验证输入、触发事件等等。

第三幕:in 操作符的妙用

in 操作符可以用来检查一个对象是否拥有某个属性。对于私有字段,in 操作符只能在类内部使用。

class MyClass {
  #privateField;

  hasPrivateField() {
    return #privateField in this; // 返回 true
  }

  static hasPrivateField(obj) {
    return #privateField in obj; // 报错:Private field '#privateField' must be declared in an enclosing class
  }
}

const myObj = new MyClass();
console.log(myObj.hasPrivateField()); // 输出 true
//console.log(MyClass.hasPrivateField(myObj)); // 报错

in 操作符对于检查私有字段的存在性非常有用,特别是在处理复杂的逻辑时。

第四幕:为什么要用“真·私有”?

  • 更强的封装性: 真正的私有,防止外部随意访问和修改内部数据,保证类的完整性和一致性。
  • 更好的代码可维护性: 修改内部实现不会影响外部代码,降低了重构的风险。
  • 更安全的代码: 避免恶意代码通过访问私有属性来破坏程序的行为。
  • 避免命名冲突: 不再需要担心私有属性的命名与其他属性冲突。

第五幕:表格对比:新旧私有方案大PK

特性 命名约定(_ 闭包 WeakMap Private Fields/Methods/Accessors(#
私有性 较强 极强
易用性 简单 一般 一般 简单
性能 较低 较高
代码复杂度 较高 一般
是否原生支持
子类访问父类私有 可以 不可以 不可以 不可以
适用场景 小型项目,不强调安全性 需要强私有性 需要一定性能 大型项目,强调安全性,需要原生支持

第六幕:一些注意事项

  • 浏览器兼容性: ES2022 的特性需要较新的浏览器版本支持。在使用前请确保目标环境支持。
  • TypeScript 的配合: TypeScript 也支持私有属性和方法,并且提供了更丰富的类型检查功能。
  • 谨慎使用私有: 过度使用私有可能会降低代码的灵活性。应该根据实际情况,选择合适的封装级别。

第七幕:案例分析:一个简单的银行账户

让我们用一个银行账户的例子来演示如何使用 Private MethodsPrivate Accessors

class BankAccount {
  #balance = 0; // 私有余额

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

  #validateAmount(amount) { // 私有方法,验证金额
    if (typeof amount !== 'number' || amount <= 0) {
      console.error("Invalid amount.");
      return false;
    }
    return true;
  }

  deposit(amount) {
    if (this.#validateAmount(amount)) {
      this.#balance += amount;
      console.log(`Deposited ${amount}. New balance: ${this.getBalance()}`);
    }
  }

  withdraw(amount) {
    if (this.#validateAmount(amount)) {
      if (amount <= this.#balance) {
        this.#balance -= amount;
        console.log(`Withdrew ${amount}. New balance: ${this.getBalance()}`);
      } else {
        console.error("Insufficient funds.");
      }
    }
  }

  getBalance() {
    return this.#balance;
  }

  // 私有 accessor ,隐藏内部计息逻辑
  get #interestRate() {
    return 0.01; // 假设利率为 1%
  }

  calculateInterest() {
    return this.#balance * this.#interestRate;
  }

  // 试图直接访问私有属性
  // externalAccess() {
  //  console.log(this.#interestRate);  //报错
  // }
}

const account = new BankAccount(100);
account.deposit(50);
account.withdraw(20);
console.log(`Current balance: ${account.getBalance()}`);
console.log(`Interest earned: ${account.calculateInterest()}`);

//account.#balance = 1000; // 报错:无法访问私有属性

在这个例子中,#balance 是私有字段,#validateAmount 是私有方法,#interestRate是私有accessor。它们只能在 BankAccount 类内部访问,保证了账户余额的安全性,以及验证金额的逻辑不被外部篡改。

总结:拥抱“真·私有”,编写更健壮的代码

Private MethodsPrivate Accessors 是 JavaScript 封装能力的重要补充。它们提供了一种更可靠、更原生的方式来隐藏类的内部实现细节,编写出更加健壮、易于维护的代码。赶紧用起来,让你的代码更上一层楼吧!

希望今天的分享对大家有所帮助!下次再见!

发表回复

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