JS中的私有属性和只读属性

什么是私有属性

私有属性是面向对象编程(OOP)中很是常见的一个特性,通常是指能被class内部的不一样方法访问,但不能在类外部被访问,大多数语言都是经过public、private、protected 这些访问修饰符来实现访问控制的。git

私有属性(方法)的意义很大程度在于将class的内部实现隐藏起来,而对外接口只经过public成员进行暴露,以减小外部对该class内部实现的依赖或修改。es6

简单地说,对于一个class来讲,外部调用方其实只关注部分,不关注class内部的具体实现,也不会使用到一些内部的变量或者方法,这种时候,若是我把全部东西都暴露给外部的话,一方面增长了使用方的理解成本和接受成本,另外一方面也增长了class自己的维护成本和被外部破坏的风险。github

所以,只暴露和用户交互的接口,其余不交互的部分隐藏为私有的,既能够促进体系运行的可靠程度,(防止外部猪队友疯狂修改你的class),也能够减少使用者的信息负载(我只须要调用一个方法来获取我要的东西,其余的我无论)。编程

Js中的私有属性

众所周知,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内部你也没法直接使用这个私有属性。

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
});
复制代码

固然其限制也很大:

  1. 没法阻止整个对象的替换,也就是obj能够被直接赋值
  2. 须要对对象的每一个属性进行设置,同时对于新增属性没法生效(除非你在新增的时候再调用一下这个)
  3. 嵌套对象也没法阻止对内部的编辑修改。

对此咱们可使用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,已防止你新增属性。参见:

github.com/GoogleChrom…

相关文章
相关标签/搜索