您现在的位置是:网站首页 > 私有属性模式(Private Property)的实现方案文章详情

私有属性模式(Private Property)的实现方案

在JavaScript中,私有属性模式用于限制对象属性的直接访问,确保数据封装和安全性。尽管ES6之前缺乏原生支持,但开发者通过闭包、Symbol、WeakMap等方式模拟私有性。ES2022正式引入了#语法糖,为私有属性提供了标准化方案。

闭包实现私有属性

通过IIFE(立即调用函数表达式)和闭包创建私有变量,是最经典的模拟方案。外部无法直接访问闭包内的变量,只能通过暴露的方法间接操作:

const Counter = (function() {
  let _count = 0; // 私有变量

  return {
    increment() {
      _count++;
      console.log(_count);
    },
    reset() {
      _count = 0;
    }
  };
})();

Counter.increment(); // 1
console.log(Counter._count); // undefined

这种方案的缺点是每个实例都会创建独立的方法副本,内存效率较低。改进方案是将方法定义在原型上:

function Counter() {
  let _count = 0;

  this.increment = function() {
    _count++;
    console.log(_count);
  };
}

Counter.prototype.reset = function() {
  // 无法访问_count!
};

Symbol实现伪私有属性

ES6的Symbol特性可用于创建"伪私有"属性,因为外部无法轻易获取Symbol引用:

const _count = Symbol('count');

class Counter {
  constructor() {
    this[_count] = 0;
  }

  increment() {
    this[_count]++;
    console.log(this[_count]);
  }
}

const c = new Counter();
c.increment(); // 1
console.log(Object.getOwnPropertySymbols(c)); // 仍可获取Symbol

虽然通过Object.getOwnPropertySymbols仍可突破,但相比普通属性已具备一定隐蔽性。

WeakMap实现真正私有

WeakMap配合闭包可实现真正的私有存储,每个实例的私有数据独立存储在WeakMap中:

const _private = new WeakMap();

class Counter {
  constructor() {
    _private.set(this, {
      count: 0
    });
  }

  increment() {
    const data = _private.get(this);
    data.count++;
    console.log(data.count);
  }
}

const c = new Counter();
c.increment(); // 1
console.log(_private.get(c)); // {count:1} 但外部无法访问_private

此方案需要为每个类创建独立的WeakMap,且实例销毁后私有数据会自动回收。

ES2022私有字段语法

最新标准通过#前缀定义真正意义上的私有属性:

class Counter {
  #count = 0; // 私有字段

  increment() {
    this.#count++;
    console.log(this.#count);
  }

  static #MAX = 100; // 静态私有字段
}

const c = new Counter();
c.increment(); // 1
console.log(c.#count); // SyntaxError

私有字段具有以下特性:

  1. 必须以#开头命名
  2. 不能在类外部通过任何方式访问
  3. 不支持动态访问如this['#x']
  4. 子类不能继承父类的私有字段

私有方法与访问器

同样的语法可应用于方法和访问器:

class Auth {
  #password;

  constructor(password) {
    this.#password = password;
  }

  #validate(pwd) {
    return this.#password === pwd;
  }

  get #encrypted() {
    return btoa(this.#password);
  }

  login(input) {
    return this.#validate(input);
  }
}

与TypeScript private的比较

TypeScript的private修饰符只是编译时约束:

class Foo {
  private secret = 123;
}
const f = new Foo();
console.log((f as any).secret); // 运行时仍可访问

而JavaScript的#私有字段是运行时强制限制,编译后字段名会被重命名为不可预测的值。

浏览器兼容性与编译方案

截至2023年,所有现代浏览器均已支持私有字段语法。对于旧环境,可通过Babel插件@babel/plugin-proposal-class-properties转译:

// 编译前
class X {
  #y = 1;
}

// 编译后
var _y = /*#__PURE__*/new WeakMap();
class X {
  constructor() {
    _y.set(this, 1);
  }
}

设计模式中的应用案例

观察者模式中保护观察者列表:

class Subject {
  #observers = [];

  addObserver(obs) {
    this.#observers.push(obs);
  }

  notify() {
    this.#observers.forEach(obs => obs.update());
  }
}

单例模式中隐藏实例引用:

class Logger {
  static #instance;

  static getInstance() {
    if (!this.#instance) {
      this.#instance = new Logger();
    }
    return this.#instance;
  }

  #logs = [];

  log(msg) {
    this.#logs.push(msg);
  }
}

性能考量

V8引擎对私有字段进行了专门优化:

  • 访问速度与普通属性相当
  • 内存占用小于WeakMap方案
  • 静态分析更容易优化

测试案例显示,连续访问私有字段比通过WeakMap获取快约30%:

class Bench {
  #value = 0;
  static #w = new WeakMap();

  constructor() {
    Bench.#w.set(this, {value:0});
  }

  incPrivate() {
    this.#value++;
  }

  incWeakMap() {
    const data = Bench.#w.get(this);
    data.value++;
  }
}

常见问题与解决方案

  1. JSON序列化问题: 私有字段默认会被JSON.stringify忽略,需自定义toJSON
class User {
  #name = 'John';

  toJSON() {
    return { name: this.#name };
  }
}
  1. 测试困难: 可通过临时暴露方法辅助测试:
class DB {
  #connection;

  constructor() {
    this.#connection = this.#init();
  }

  #init() { /*...*/ }

  // 仅用于测试
  __testExposeConnection() {
    return this.#connection;
  }
}
  1. 多类共享私有字段: 通过继承基类实现:
class PrivateBase {
  #shared;
  constructor(v) { this.#shared = v; }
}

class A extends PrivateBase {
  method() {
    console.log(this.#shared);
  }
}

与其他语言的对比

Java的private允许同包访问,C++的private允许友元访问,而JavaScript的#是严格的类级私有。Python通过命名约定(_prefix)实现约定俗成的私有性,JavaScript的私有字段则是语法强制。

设计决策建议

  1. 新项目直接使用#语法
  2. 库开发考虑WeakMap方案以兼容旧环境
  3. 需要动态访问时采用Symbol方案
  4. 简单工具函数仍可使用闭包模式

我的名片

网名:~川~

岗位:console.log 调试员

坐标:重庆市-九龙坡区

邮箱:cc@qdcc.cn

沙漏人生

站点信息

  • 建站时间:2013/03/16
  • 本站运行
  • 文章数量
  • 总访问量
微信公众号
每次关注
都是向财富自由迈进的一步