您现在的位置是:网站首页 > 私有属性模式(Private Property)的实现方案文章详情
私有属性模式(Private Property)的实现方案
陈川
【
JavaScript
】
51210人已围观
4421字
在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
私有字段具有以下特性:
- 必须以
#
开头命名 - 不能在类外部通过任何方式访问
- 不支持动态访问如
this['#x']
- 子类不能继承父类的私有字段
私有方法与访问器
同样的语法可应用于方法和访问器:
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++;
}
}
常见问题与解决方案
- JSON序列化问题:
私有字段默认会被
JSON.stringify
忽略,需自定义toJSON
:
class User {
#name = 'John';
toJSON() {
return { name: this.#name };
}
}
- 测试困难: 可通过临时暴露方法辅助测试:
class DB {
#connection;
constructor() {
this.#connection = this.#init();
}
#init() { /*...*/ }
// 仅用于测试
__testExposeConnection() {
return this.#connection;
}
}
- 多类共享私有字段: 通过继承基类实现:
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的私有字段则是语法强制。
设计决策建议
- 新项目直接使用
#
语法 - 库开发考虑WeakMap方案以兼容旧环境
- 需要动态访问时采用Symbol方案
- 简单工具函数仍可使用闭包模式
上一篇: 性能监控与分析工具