本文详细解析JavaScript中实现私有属性的6种方式。从简单的下划线命名规范,到利用Proxy拦截访问、Symbol生成唯一键、WeakMap存储数据,再到ES新语法的#prop以及TypeScript的private修饰符。文章对比了各方案的优缺点,区分了伪私有与真正私有,并提供了代码示例与编译原理说明,帮助开发者选择最适合的私有化方案。
类是对象的模板,由属性和方法构成,表示对同一概念的数据和操作。
部分属性和方法对外公开,部分仅内部使用,即私有属性,需探讨其实现方式。
梳理出6种实现私有属性和方法的方式,分别进行解析。
_prop
区分私有与公有最简单的方式是添加下划线 _,通过命名进行区分。
例如:
class Dong {
constructor() {
this._name = 'dong';
this._age = 20;
this.friend = 'guang';
}
hello() {
return 'I\'m ' + this._name + ', ' + this._age + ' years old';
}
}
const dong = new Dong();
console.log(dong.hello());此处 Dong 类拥有私有属性 _name、_age 以及公有属性 friend。
此方式仅为命名规范,提示开发者属性为私有,非强制限制,仍可被调用。

该方式应用广泛且历史较为悠久。
基于此规范实现真正私有,需借助 Proxy。
Proxy
Proxy 可定义目标对象的 get、set、Object.keys 逻辑,通过判断下划线 _ 开头限制访问。
例如沿用上述 class:
class Dong {
constructor() {
this._name = 'dong';
this._age = 20;
this.friend = 'guang';
}
hello() {
return 'I\'m ' + this._name + ', ' + this._age + ' years old';
}
}
const dong = new Dong();不直接调用对象属性方法,而是使用 Proxy 约束 get、set、getKeys 行为:
const dong = new Dong();
const handler = {
get(target, prop) {
if (prop.startsWith('_')) {
return;
}
return target[prop];
},
set(target, prop, value) {
if (prop.startsWith('_')) {
return;
}
target[prop] = value;
},
ownKeys(target, prop) {
return Object.keys(target).filter(key => !key.startsWith('_'))
},
}
const proxy = new Proxy(dong, handler)通过 new Proxy 为 dong 定义 get、set、ownKeys 的 handler:
- get: 若属性以下划线 _ 开头则返回空,否则返回目标对象属性值 target[prop]
- set: 若属性以下划线 _ 开头则直接返回,否则设置目标对象属性值
- ownKeys: 访问 keys 时,过滤掉目标对象中下划线开头的属性后返回
由此实现以下划线开头属性的私有化:
进行测试:
const proxy = new Proxy(dong, handler)
for (const key of Object.keys(proxy)) {
console.log(key, proxy[key])
}
结果显示仅打印公有属性方法,下划线开头的两个属性未打印。
基于 _prop 命名规范实现了真正的私有属性!
进一步调用方法测试:

为何返回 undefined?
因 proxy.hello 方法的 this 指向 proxy,受限制,需额外处理:

若使用方法,需将其 this 绑定为目标对象。
如此 hello 方法即可访问 _ 开头的私有属性:

通过 Proxy 实现了下划线命名规范的真正私有属性,但定义 Proxy 较繁琐,有无无需定义 Proxy 的方式?
确实存在,例如 Symbol:
Symbol
Symbol 是 es2015 添加的 API,用于创建唯一值,基于此特性可实现私有属性。
例如如下:
const nameSymbol = Symbol('name');
const ageSymbol = Symbol('age');
class Dong {
constructor() {
this[nameSymbol] = 'dong';
this[ageSymbol] = 20;
}
hello() {
return 'I\'m ' + this[nameSymbol] + ', ' + this[ageSymbol] + ' years old';
}
}
const dong = new Dong();不再使用 name 和 age 作为私有属性名,而是用 Symbol 生成唯一值作为名称。
外部因无法获取属性名,故无法取得对应属性值:

此方式比 Proxy 更简单,也是常用的私有属性实现方式之一。
若需暴露,可定义 get 方法:

但这种私有属性是否真的无法访问?
并非如此,存在 Object.getOwnPropertySymbols API,可获取对象所有 Symbols 属性,进而获取属性值:

因此该方式仅是 Object.keys 无法获取对应属性,不如 Proxy 方式完善。
不使用 Proxy 且比 Symbol 更完善的方式是否存在?
可尝试以下方式:
WeakMap
外部能访问属性和方法是因为将其挂载到 this 上,若不挂载到 this 则无法访问。
例如使用 Map 保存私有属性:
const privateFields = new Map();
class Dong {
constructor() {
privateFields.set('name', 'dong');
privateFields.set('age', 20);
}
hello() {
return 'I\'m ' + privateFields.get('name') + ', ' + privateFields.get('name') + ' years old';
}
}测试如下:

看似可行,但存在以下问题:
- 所有对象共用同一个 Map,相互之间会产生影响。
- 对象销毁后,该 Map 依然存在。
如何解决上述问题?
可尝试使用 WeakMap,其特性是仅能以对象作为 key,对象销毁时键值对随之销毁。
完美解决上述两个问题:
- 因以对象作为 key,不同对象位于不同键值对,互不影响。
- 对象销毁时,对应键值对自动销毁,无需手动管理。
实现如下:
const dongName = new WeakMap();
const dongAge = new WeakMap();
const classPrivateFieldSet = function(receiver, state, value) {
state.set(receiver, value);
}
const classPrivateFieldGet = function(receiver, state) {
return state.get(receiver);
}
class Dong {
constructor() {
dongName.set(this, void 0);
dongAge.set(this, void 0);
classPrivateFieldSet(this, dongName, 'dong');
classPrivateFieldSet(this, dongAge, 20);
}
hello() {
return 'I\'m ' + classPrivateFieldGet(this, dongName) + ', ' + classPrivateFieldGet(this, dongAge) + ' years old';
}
}每个属性定义一个 WeakMap 进行维护,key 为当前对象,值为属性值,get 和 set 使用 classPrivateFieldSet 和 classPrivateFieldGet 方法,最终通过从 WeakMap 中存取。
在构造器中初始化当前对象对应的属性值,即 dongName.set(this, void 0),此处 void 0 返回值为 undefined,含义相同。
进行测试:

通过 WeakMap 也能实现私有属性!
但此处 classPrivateFieldGet 是否必要?直接 xxMap.get 即可。
确实,封装一层旨在添加额外逻辑,此处也可直接从 weakMap 获取。
但这样编写仍显繁琐,有无更简单的方式?
能否设计一种语法糖,自动编译为此种方式?
设想正确,确实存在此类语法糖:
#prop
目前存在私有属性的 ES 草案,可通过 # 标识私有属性和方法。
例如如下:
class Dong {
constructor() {
this.#name = 'dong';
this.#age = 20;
this.friend = 'guang';
}
hello() {
return 'I\'m ' + this.#name + this.#age + 'years old';
}
}此处 name 和 age 均为私有,而 friend 为公有。
新语法 JS 引擎支持尚需时日,但可通过 babel 或 ts 编译器编译为低版本语法提前使用。
例如 babel 提供 @babel/proposal-private-property-in-object 插件,可实现该语法编译:

babel 将 #prop 编译为上述 WeakMap 方式实现。
该插件包含在 @babel/preset-env 预设中,会自动引入:

除 babel 外,ts 中也可直接使用此语法:

同样会编译为 WeakMap 方式实现。
ts 实现的新语法较多,例如 ? 和 ?? 分别为可选链和默认值语法,以下两种写法等价:
const res = data?.name ?? 'dong';
const res2 = data && data.name || 'dong';此类新语法可直接使用,babel 则需引入 proposal 插件。
另外,记得 ts 中 class 有 private 修饰符,那是否也是私有属性?
其实它是私有属性但也不完全,我们来看一下:
ts private
ts 可通过 private 修饰属性、方法的可见性:
- private 表示属性私有,仅 class 内部可访问
- protected 表示保护,仅 class 和子 class 可访问
- public 表示公有,外部也可访问
类型检查和提示时有区别,例如 private 属性在 class 外部不可访问:

而 class 内部是可以访问的:

但此约束仅用于类型检查,仅存在于编译期间,运行时并无此约束。
可查看编译后的代码:

可见未做任何处理。
若使用 #prop 方式,除编译时是 private 的,运行时也是:

因此,要实现真正的 private,仍应用 #prop 方式,若仅需编译时约束则声明 private 即可。
总结
class 用于定义围绕某概念的一系列属性和方法,部分内部使用,部分对外公开。仅内部使用的属性、方法需实现私有化。
实现私有属性方法,共梳理出 6 种方式:
- 通过下划线 _prop 从命名上区分
- 通过 Proxy 来定义 get、set、ownKeys 的逻辑
- 通过 Symbol 来定义唯一的属性名,不能通过 keys 拿到
- 通过 WeakMap 来保存所有对象的私有属性和方法
- 通过 #prop 的 es 新语法实现私有,babel 和 tsc 会把它们编译成 WeakMap 的方式
- 通过 ts 的 private 在编译时约束
这六种方式中,有三种仅为伪私有,例如 _prop(仍可访问)、ts 的 private(运行时可访问)、Symbol(可通过 Object.getOwnSymbols 拿到 symbol 来访问)。
另外三种是真正的私有,包括 Proxy、WeakMap、#prop(目前编译为 WeakMap 方式)。
有的从属性名入手,如 _prop 和 Symbol;有的从 this 入手,如 Proxy(包一层)和 WeakMap(不挂到 this);有的从语言本身入手,如 ts 的 private 或 es 新语法的 #prop。
这 6 种实现私有属性的方式,你用过几种?
以上是一文详解JS私有属性的6种实现方式的详细内容,更多关于JS私有属性的资料请关注本站其它相关文章!