從ES6從新認識JavaScript設計模式(五): 代理模式和Proxy

1 什麼是代理模式

an object

爲其餘對象提供一種代理以控制對這個對象的訪問。在某些狀況下,一個對象不適合或者不能直接引用另外一個對象,而代理對象能夠在客戶端和目標對象之間起到中介的做用。

在生活中,代理模式的場景是十分常見的,例如咱們如今若是有租房、買房的需求,更多的是去找鏈家等房屋中介機構,而不是直接尋找想賣房或出租房的人談。此時,鏈家起到的做用就是代理的做用。鏈家和他所代理的客戶在租房、售房上提供的方法可能都是一致的(收錢,籤合同),但是鏈家做爲代理卻提供了訪問限制,讓咱們不能直接訪問被代理的客戶。前端

在面向對象的編程中,代理模式的合理使用可以很好的體現下面兩條原則:es6

  • 單一職責原則: 面向對象設計中鼓勵將不一樣的職責分佈到細粒度的對象中,Proxy 在原對象的基礎上進行了功能的衍生而又不影響原對象,符合鬆耦合高內聚的設計理念。
  • 開放-封閉原則:代理能夠隨時從程序中去掉,而不用對其餘部分的代碼進行修改,在實際場景中,隨着版本的迭代可能會有多種緣由再也不須要代理,那麼就能夠容易的將代理對象換成原對象的調用

2 ES6中的代理模式

ES6所提供Proxy構造函數可以讓咱們輕鬆的使用代理模式:ajax

var proxy = new Proxy(target, handler);

Proxy構造函數傳入兩個參數,第一個參數target表示所要代理的對象,第二個參數handler也是一個對象用來設置對所代理的對象的行爲。若是想知道Proxy的具體使用方法,可參考阮一峯的《 ECMAScript入門 - Proxy 》編程

本文將利用Proxy實現前端中3種代理模式的使用場景,分別是:緩存代理驗證代理實現私有屬性後端

2.1 緩存代理

緩存代理能夠將一些開銷很大的方法的運算結果進行緩存,再次調用該函數時,若參數一致,則能夠直接返回緩存中的結果,而不用再從新進行運算。例如在採用後端分頁的表格時,每次頁碼改變時須要從新請求後端數據,咱們能夠將頁碼和對應結果進行緩存,當請求同一頁時就不用在進行ajax請求而是直接返回緩存中的數據。設計模式

下面咱們以沒有通過任何優化的計算斐波那契數列的函數來假設爲開銷很大的方法,這種遞歸調用在計算40以上的斐波那契項時就能明顯的感到延遲感。緩存

const getFib = (number) => {
  if (number <= 2) {
    return 1;
  } else {
    return getFib(number - 1) + getFib(number - 2);
  }
}

如今咱們來寫一個建立緩存代理的工廠函數:app

const getCacheProxy = (fn, cache = new Map()) => {
  return new Proxy(fn, {
    apply(target, context, args) {
      const argsString = args.join(' ');
      if (cache.has(argsString)) {
        // 若是有緩存,直接返回緩存數據
        console.log(`輸出${args}的緩存結果: ${cache.get(argsString)}`);
        
        return cache.get(argsString);
      }
      const result = fn(...args);
      cache.set(argsString, result);

      return result;
    }
  })
}

調用方法以下:函數

const getFibProxy = getCacheProxy(getFib);
getFibProxy(40); // 102334155
getFibProxy(40); // 輸出40的緩存結果: 102334155

當咱們第二次調用getFibProxy(40)時,getFib函數並無被調用,而是直接從cache中返回了以前被緩存好的計算結果。經過加入緩存代理的方式,getFib只須要專一於本身計算斐波那契數列的職責,緩存的功能使由Proxy對象實現的。這實現了咱們以前提到的單一職責原則post

2.2 驗證代理

Proxy構造函數第二個參數中的set方法,能夠很方便的驗證向一個對象的傳值。咱們以一個傳統的登錄表單舉例,該表單對象有兩個屬性,分別是accountpassword,每一個屬性值都有一個簡單和其屬性名對應的驗證方法,驗證規則以下:

// 表單對象
const userForm = {
  account: '',
  password: '',
}

// 驗證方法
const validators = {
  account(value) {
    // account 只容許爲中文
    const re = /^[\u4e00-\u9fa5]+$/;
    return {
      valid: re.test(value),
      error: '"account" is only allowed to be Chinese'
    }
  },
  password(value) {
    // password 的長度應該大於6個字符
    return {
      valid: value.length >= 6,
      error: '"password "should more than 6 character'
    }
  }
}

下面咱們來使用Proxy實現一個通用的表單驗證器

const getValidateProxy = (target, validators) => {
  return new Proxy(target, {
    _validators: validators,
    set(target, prop, value) {
      if (value === '') {
        console.error(`"${prop}" is not allowed to be empty`);
        return target[prop] = false;
      }
      const validResult = this._validators[prop](value);
      if(validResult.valid) {
        return Reflect.set(target, prop, value);
      } else {
        console.error(`${validResult.error}`);
        return target[prop] = false;
      }
    }
  })
}

調用方式以下

const userFormProxy = getValidateProxy(userForm, validators);
userFormProxy.account = '123'; // "account" is only allowed to be Chinese
userFormProxy.password = 'he'; // "password "should more than 6 character

咱們調用getValidateProxy方法去生成了一個代理對象userFormProxy,該對象在設置屬性的時候會根據validators的驗證規則對值進行校驗。這咱們使用的是console.error拋出錯誤信息,固然咱們也能夠加入對DOM的事件來實現頁面中的校驗提示。

2.3 實現私有屬性

代理模式還有一個很重要的應用是實現訪問限制。總所周知,JavaScript是沒有私有屬性這一個概念的,一般私有屬性的實現是經過函數做用域中變量實現的,雖然實現了私有屬性,但對於可讀性來講並很差。

私有屬性通常是以_下劃線開頭,經過Proxy構造函數中的第二個參數所提供的方法,咱們能夠很好的去限制以_開頭的屬性的訪問。

下面我來實現getPrivateProps這個函數,該函數的第一個參數obj是所被代理的對象,第二個參數filterFunc是過濾訪問屬性的函數,目前該函數的做用是用來限制以_開頭的屬性訪問。

function getPrivateProps(obj, filterFunc) {
  return new Proxy(obj, {
    get(obj, prop) {
      if (!filterFunc(prop)) {
        let value = Reflect.get(obj, prop);
        // 若是是方法, 將this指向修改原對象
        if (typeof value === 'function') {
          value = value.bind(obj);
        }
        return value;
      }
    },
    set(obj, prop, value) {
      if (filterFunc(prop)) {
        throw new TypeError(`Can't set property "${prop}"`);
      }
      return Reflect.set(obj, prop, value);
    },
    has(obj, prop) {
      return filterFunc(prop) ? false : Reflect.has(obj, prop);
    },
    ownKeys(obj) {
      return Reflect.ownKeys(obj).filter(prop => !filterFunc(prop));
    },
    getOwnPropertyDescriptor(obj, prop) {
      return filterFunc(prop) ? undefined : Reflect.getOwnPropertyDescriptor(obj, prop);
    }
  });
}

function propFilter(prop) {
  return prop.indexOf('_') === 0;
}

在上面的getPrivateProps方法的內部實現中, Proxy的第二個參數中咱們使用了提供的get,set,has,ownKeys, getOwnPropertyDescriptor這些方法,這些方法的做用其實本質都是去最大限度的限制私有屬性的訪問。其中在get方法的內部,咱們有個判斷,若是訪問的是對象方法使將this指向被代理對象,這是在使用Proxy須要十分注意的,若是不這麼作方法內部的this會指向Proxy代理。

下面來看一下getPrivateProps的調用方法,並驗證其代理提供的訪問控制的能力。

const myObj = {
  public: 'hello',
  _private: 'secret',
  method: function () {
    console.log(this._private);
  }
},

myProxy = getPrivateProps(myObj, propFilter);

console.log(JSON.stringify(myProxy)); // {"public":"hello"}
console.log(myProxy._private); // undefined
console.log('_private' in myProxy); // false
console.log(Object.keys(myProxy)); // ["public", "method"]
for (let prop in myProxy) { console.log(prop); }    // public  method
myProxy._private = 1; // Uncaught TypeError: Can't set property "_private"

3 總結

ES6提供的Proxy可讓JS開發者很方便的使用代理模式,據說Vue 3.0的也會使用Proxy去大量改寫核心代碼。雖然代理模式很方便,可是在業務開發時應該注意使用場景,不須要在編寫對象時就去預先猜想是否須要使用代理模式,只有當對象的功能變得複雜或者咱們須要進行必定的訪問限制時,再考慮使用代理。


參考文獻

相關文章
相關標籤/搜索