私有属性是面向对象编程(OOP
)中很是常见的一个特性,通常是指能被class
内部的不一样方法访问,但不能在类外部被访问,大多数语言都是经过public、private、protected 这些访问修饰符来实现访问控制的。git
私有属性(方法)的意义很大程度在于将class的内部实现隐藏起来,而对外接口只经过public成员进行暴露,以减小外部对该class内部实现的依赖或修改。es6
简单地说,对于一个class来讲,外部调用方其实只关注部分,不关注class内部的具体实现,也不会使用到一些内部的变量或者方法,这种时候,若是我把全部东西都暴露给外部的话,一方面增长了使用方的理解成本和接受成本,另外一方面也增长了class自己的维护成本和被外部破坏的风险。github
所以,只暴露和用户交互的接口,其余不交互的部分隐藏为私有的,既能够促进体系运行的可靠程度,(防止外部猪队友疯狂修改你的class),也能够减少使用者的信息负载(我只须要调用一个方法来获取我要的东西,其余的我无论)。编程
众所周知,JavaScript 中没有 public、private、protected 这些访问修饰符(access modifiers),并且长期以来也没有私有属性这个概念,对象的属性/方法默认都是public的。数组
这意味着你写一个function或者class,外部其实能够任意访问,任意修改,因此你能够在js中看到不少看起来很hack的写法,从外部修改一个值,莫名的内部的运行逻辑就变化了。这自己是一件很是危险的事,同时对于一个开发而言,怎么能容许,因此经过对逻辑和数据进行必定封装和魔改,JS开发者们走上了曲线实现“私有属性”之路。babel
自欺欺人型,我说他是私有,那么他就是私有,不容许辩驳。或者说约定俗成,以一种不成文的规定,在变量前加上下划线"_"前缀,约定这是一个私有属性;可是实际上这一类属性与正常属性没有任何区别,你在外部仍然能够访问,仅仅是指你在访问是看到了这个前缀,哦,原来这是一个私有,我不应直接访问。markdown
class Person {
_name;
constructor(name) {
this._name = name;
}
复制代码
理论上,ts的private修饰符也是这一类,尽管ts实现了private,public等访问修饰符,可是实质只会在编译阶段进行检查,编译后的结果是不会实现访问控制的,也就是运行时是彻底没用的。闭包
闭包是指在 JavaScript 中,内部函数老是能够访问其所在的外部函数中声明的参数和变量,即便在其外部函数被返回(寿命终结)了以后.函数
基于这个特性,经过建立一个闭包,咱们能模拟实现一个私有变量oop
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内部你也没法直接使用这个私有属性。
咱们能够知道,实现私有属性,只要是外部没法知道这个属性名,只有内部知道的属性名,就能够作到外部没法访问的特性,基于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
复制代码
固然好消息是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了。
要设置一个对象的值可读,咱们能够用更简单的办法,使用defineProperty,将其writable设为false
var obj = {};
Object.defineProperty( obj, "<属性名>", {
value: "<属性值>",
writable: false
});
复制代码
固然其限制也很大:
对此咱们可使用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,已防止你新增属性。参见: