什么是私有属性
私有属性是面向对象编程(OOP
)中非常常见的一个特性,一般是指能被class
内部的不同方法访问,但不能在类外部被访问,大多数语言都是通过public、private、protected 这些访问修饰符来实现访问控制的。
私有属性(方法)的意义很大程度在于将class的内部实现隐藏起来,而对外接口只通过public成员进行暴露,以减少外部对该class内部实现的依赖或修改。
简单地说,对于一个class来说,外部调用方其实只关注部分,不关注class内部的具体实现,也不会使用到一些内部的变量或者方法,这种时候,如果我把所有东西都暴露给外部的话,一方面增加了使用方的理解成本和接受成本,另一方面也增加了class本身的维护成本和被外部破坏的风险。
因此,只暴露和用户交互的接口,其他不交互的部分隐藏为私有的,既可以促进体系运行的可靠程度,(防止外部猪队友疯狂修改你的class),也可以减小使用者的信息负载(我只需要调用一个方法来获取我要的东西,其他的我不管)。
Js中的私有属性
众所周知,JavaScript 中没有 public、private、protected 这些访问修饰符(access modifiers),而且长期以来也没有私有属性这个概念,对象的属性/方法默认都是public的。
这意味着你写一个function或者class,外部其实可以任意访问,任意修改,所以你可以在js中看到很多看起来很hack的写法,从外部修改一个值,莫名的内部的运行逻辑就变化了。这本身是一件非常危险的事,同时对于一个开发而言,怎么能允许,所以通过对逻辑和数据进行一定封装和魔改,JS开发者们走上了曲线实现“私有属性”之路。
自欺欺人型
自欺欺人型,我说他是私有,那么他就是私有,不允许辩驳。或者说约定俗成,以一种不成文的规定,在变量前加上下划线"_"前缀,约定这是一个私有属性;但是实际上这一类属性与正常属性没有任何区别,你在外部仍然可以访问,仅仅是指你在访问是看到了这个前缀,哦,原来这是一个私有,我不该直接访问。
class Person {
_name;
constructor(name) {
this._name = name;
}
理论上,ts的private修饰符也是这一类,尽管ts实现了private,public等访问修饰符,但是实质只会在编译阶段进行检查,编译后的结果是不会实现访问控制的,也就是运行时是完全没用的。
闭包
闭包是指在 JavaScript 中,内部函数总是可以访问其所在的外部函数中声明的参数和变量,即使在其外部函数被返回(寿命终结)了之后.
基于这个特性,通过创建一个闭包,我们能模拟实现一个私有变量
var Person = function(name){
var _name = name;
this.getName = function(){
return _name;
};
this.setName = function(str){
_name = str;
};
};
var person = new Person('hugh');
person.name; // undefined, name是Person函数内的变量,外部无法直接访问
person.getName(); // 'hugh'
person.setName('test');
或者 class形式的
class Person {
constructor(name) {
// 私有属性
let _name = name;
this.setName = function (name) {
_name = name;
};
this.getName = function () {
return _name;
};
}
greet() {
console.log(`hi, i'm ${this.getName()}`);
}
}
闭包是一个比较好的实现,借助js本身的特性实现了访问控制,但是同样毕竟是个魔改,仍然遗留了一点问题,你需要为每个变量定义getter和setter,否则你甚至无法在class内部获取到整个私有变量,但是当你定义了getter,外部也可以通过这个getter来获取私有变量。
所以闭包实现了你无法直接读取内部的私有属性,同样,在class内部你也无法直接使用这个私有属性。
symbol和weakMap
我们可以知道,实现私有属性,只要是外部无法知道这个属性名,只有内部知道的属性名,就可以做到外部无法访问的特性,基于ES6的新语法symbol和weakMap,我们可以去实现这个能力。
基于symbol
Symbol
是ES6 引入了一种新的原始数据类型,表示独一无二的值,并且可以所谓对象的属性名。一个完全独一无二,并且除了通过变量直接获取,无法被明确表示的属性名。完美。
var Person = (function(){
const _name = Symbol('name');
class Person {
constructor(name){
this[_name] = name;
}
get name(){
return this[_name]
}
}
return Person
}())
let person = new Person('hugh');
person.name // hugh
基于WeakMap
WeakMap也是和symbol一样,是一个把对象作为key的map,所以我们可以把实例本身作为key
var Person = (function(){
const _name = new WeakMap();
class Person {
constructor(name){
_name.set(this, name)
}
get name(){
return _name.get(this)
}
}
return Person
}())
let person = new Person('hugh');
person.name // hugh
class提案
当然好消息是ES2019中已经增加了对 class 私有属性的原生支持,只需要在属性/方法名前面加上 '#' 就可以将其定义为私有,并且支持定义私有的 static 属性/方法,同时我们现在也可以通过 Babel 已使用(babel会把#编译成上面weakMap的形式来实现私有属性),并且 Node v12 中也增加了对私有属性的支持。
class Person {
// 私有属性
#name;
constructor(name) {
this.#name = name;
}
get name(){
return this.#name;
}
}
至于为什么是#,而不是常用的private修饰符,可以看这篇文章 zhuanlan.zhihu.com/p/47166400
只读属性
只读属性与上面私有变量有点类似,逻辑上你只要给你的私有属性增加一个getter,而不增加setter那么他就是一个只读属性。
class Person {
constructor(name) {
// 私有属性
let _name = name;
this.name = function () {
return _name;
};
}
}
比较麻烦的是你必须使用getter方法来获取属性,当然我们可以通过class的get来简化这个
class Person {
// 私有属性
#name;
constructor(name) {
this.#name = name;
}
get name(){
return this.#name;
}
}
然而对于简单类型这个就是比较完美的只读属性了,但是对于对象,数组等复杂类型,你仍然可以通过外部去增加属性。
class Person {
// 私有属性
#name;
constructor() {
this.#name = {};
}
get name(){
return this.#name;
}
}
let person = new Person();
person.name.title = 'hugh';
person.name // {title:'hugh'}
为了让对象类型的属性不可变,我们可以将这个属性freeze
使用Object.freeze()冻结的对象中的现有属性值是不可变的,不可编辑,不可新增。用Object.seal()密封的对象可以改变其现有属性值,但是不可新增。
class Person {
// 私有属性
#name;
constructor() {
this.#name = {title:'hugh'};
Object.freeze(this.#name)
}
get name(){
return this.#name;
}
}
当你freeze这个属性后,会造成一个问题就是,你在class内部也无法修改这个属性了,所以如果你是希望外部只读,但是会有方法可以修改这个值的话,那么就不可以使用freeze了。
Object.defineProperty与proxy
要设置一个对象的值可读,我们可以用更简单的办法,使用defineProperty,将其writable设为false
var obj = {};
Object.defineProperty( obj, "<属性名>", {
value: "<属性值>",
writable: false
});
当然其限制也很大:
- 无法阻止整个对象的替换,也就是obj可以被直接赋值
- 需要对对象的每个属性进行设置,同时对于新增属性无法生效(除非你在新增的时候再调用一下这个)
- 嵌套对象也无法阻止对内部的编辑修改。
对此我们可以使用es6的proxy来进行优化,proxy能实现defineProperty的大多数功能,又没有以上的问题
var obj = {};
const objProxy = new Proxy(obj, {
get(target,propKey,receiver) {
return Reflect.get(target, propKey, receiver);
},
set() { // 拦截写入属性操作
console.error('obj is not writeable');
return true;
},
});
对此,我们就不需要关心obj内部属性的新增了(尽管,对于嵌套对象,仍然无法阻止)
基于以上方案,我们可以对一开始的只读属性进行优化
class Person {
// 私有属性
#name;
constructor() {
this.#name = {};
}
get name(){
return new Proxy(this.#name, {
get(target,propKey,receiver) {
return Reflect.get(target, propKey, receiver);
},
set() { // 拦截写入属性操作
console.error('obj is not writeable');
return true;
},
});
}
addName(name){
this.#name[name] = name;
}
}
let person = new Person();
person.name.title = 'hugh'; // obj is not writeable
person.addName('hugh')
对于proxy的兼容我们可以引入Google的proxy-polyfill,但是需要注意,proxy-polyfill由于需要遍历对象的所有属性,为每个属性设置defineProperty,所以必须是已知对象的属性,对于对象内新增的属性无法监听,所以proxy-pollyfill seal了target和proxy,已防止你新增属性。参见: