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…

相關文章
相關標籤/搜索