在JavaScript中,类(Class)作为ES6引入的重要特性之一,为我们提供了更加面向对象的编程范式。然而,与一些其他编程语言(如Java或C#)不同,ES6中的类并没有直接支持私有属性或方法。尽管如此,随着JavaScript语言的不断演进,我们仍然可以通过一些技巧和模式来实现类似私有属性的功能。
Proxy
Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。MDN地址
我们可以先给私有变量一个特殊的命名格式,例如前面加一个下划线 _
,当Proxy劫持对象后,根据用户访问的属性名的格式来判断用户即将访问的属性是否是私有属性:
class Stu {
constructor(name, age, sexy) {
this._name = name;
this._age = age;
this.sexy = sexy
}
get info() {
return `该学生姓名:${this._name},年龄:${this._age}`;
}
}
// 定义一个处理器
const handler = {
get (target, key) {
if (key[0] === '_') {
throw new Error('禁止访问!');
}
return target[key];
},
set (target, key, value) {
if (key[0] === '_') {
throw new Error('禁止访问!');
}
target[key] = value;
},
ownKeys(target, prop) {
return Object.keys(target).filter(key => key[0] !== '_')
},
}
const zs = new Proxy(new Stu('张三', 18, '男'), handler);
console.log(zs.info); // 该学生姓名:张三,年龄:18
console.log(Object.keys(zs)) // [ 'sexy' ]
zs._age = 20; // 错误:禁止访问!
通过以上代码就实现了通过Proxy去控制对象内属性的访问权限,当用户尝试访问以 _
开头的属性时就会抛出禁止访问的错误,当然,也可以在get和set中进行其他的处理,例如不抛出错误,而是返回 undefined
。当定义了 ownKeys
时,使用 Object.keys()
时就会过滤掉目标对象中下划线开头的属性再返回。
Symbol
Symbol
是ES6中新增加的一种数据类型,用于创建一个唯一的值,那么怎么通过这个特性来创建私有变量呢?例如如下代码:
const nameSymbol = Symbol('name');
const ageSymbol = Symbol('age');
class Stu {
constructor(name, age) {
this[nameSymbol] = name;
this[ageSymbol] = age;
}
get info() {
return `该学生姓名:${this[nameSymbol]},年龄:${this[ageSymbol]}`;
}
}
const zs = new Stu('张三', 18);
console.log(zs.info); // 该学生姓名:张三,年龄:18
console.log(Object.keys(zs)) // []
console.log(zs.age); // undefined
在如上代码中,我们没有直接使用 name
和 age
来作为属性名,而是使用了Symbol生成了唯一的名字,所以在外部是拿不到属性名的,相比于proxy方式更加简单,但是,有一个api叫做 Object.getOwnPropertySymbols
,通过这个api可以获取到对象中所有Symbol属性:
const allSymbol = Object.getOwnPropertySymbols(zs);
console.log(allSymbol); // [ Symbol(name), Symbol(age) ]
console.log(zs[allSymbol[0]]); // 张三
console.log(zs[allSymbol[1]]); // 18
那么有没有更加完善的方法呢?
WeakMap
在MDN私有属性一节中,提到了在 #
这个语法出现之前,可以通过 WeakMap
来创建私有变量。WeakMap与Map的区别就是 只能用对象作为 key,对象销毁,这个键值对就销毁。 所以使用 WeakMap
可以避免所有的实例对象都共有同一个Map的问题,代码实现如下:
const wmName = new WeakMap();
const wmAge = new WeakMap();
const wmSet = function (receiver, key, value) {
key.set(receiver, value);
};
const wmGet = function (receiver, key) {
return key.get(receiver);
};
class Stu {
constructor(name, age) {
wmName.set(this, undefined);
wmAge.set(this, undefined);
wmSet(this, wmName, name);
wmSet(this, wmAge, age);
}
get info() {
return `该学生姓名:${wmGet(this, wmName)},年龄:${wmGet(this, wmAge)}`;
}
}
const zs = new Stu("张三", 18);
console.log(zs.info); // 该学生姓名:张三,年龄:18
console.log(Object.keys(zs)); // []
console.log(zs.age); // undefined
我们针对每一个私有变量,定义了一个 WeakMap
,在对象内部通过 wmGet
和 wmSet
两个函数来进行私有变量的读写,而在对象外部则无法访问到私有变量。这种办法相对于前两种办法更加完善,在js引擎未完全支持前,babel对 #
语法进行转换时就会转为 WeakMap
的实现方式,可以理解为 #
是 WeakMap
的语法糖。
#
在最新的ES标准中,可以通过 #
的方式来标识私有属性和方法。例如:
class Stu {
#name;
#age;
constructor(name, age) {
this.#name = name;
this.#age = age;
this.sexy = '男';
}
get info() {
return `该学生姓名:${this.#name},年龄:${this.#age}`;
}
}
const zs = new Stu('张三', 18);
console.log(zs.info); // 该学生姓名:张三,年龄:18
console.log(Object.keys(zs)) // [ 'sexy' ]
console.log(zs.age); // undefined
虽然使用 #
可以很方便的创建私有变量,但是需要注意该语法的浏览器兼容性,大多是2020年之前的浏览器版本都没有提供相应的支持。
至于为什么要用 #
,TC39 委员会解释道,他们也是做了深思熟虑最终选择了 # 符号,而没有使用 private 关键字。其中还讨论了把 private 和 # 符号一起使用的方案。并且还打算预留了一个 @ 关键字作为 protected 属性 。