class 是创建对象的模版,由一系列属性和方法构成,用于表示对同一概念的数据和操作。
有的属性和方法是对外的,但也有的是只想内部用的,也就是私有的,那怎么实现私有属性和方法呢?
不知道大家会怎么实现,我梳理了下,我大概用过 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 比较麻烦,有没有不定义 Prxoy 的方式呢?
确实有,比如 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 方法:
但是这种私有属性是真的没法访问么?
不是的,有一个 api 叫做 Object.getOwnPropertySymbols,可以取到对象的所有 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 的方式)。
这 6 种实现私有属性的方式,你用过几种?