爲其餘對象提供一種代理以控制對這個對象的訪問。在某些狀況下,一個對象不適合或者不能直接引用另外一個對象,而代理對象能夠在客戶端和目標對象之間起到中介的做用。
在生活中,代理模式的場景是十分常見的,例如咱們如今若是有租房、買房的需求,更多的是去找鏈家等房屋中介機構,而不是直接尋找想賣房或出租房的人談。此時,鏈家起到的做用就是代理的做用。鏈家和他所代理的客戶在租房、售房上提供的方法可能都是一致的(收錢,籤合同),但是鏈家做爲代理卻提供了訪問限制,讓咱們不能直接訪問被代理的客戶。前端
在面向對象的編程中,代理模式的合理使用可以很好的體現下面兩條原則:es6
ES6所提供Proxy
構造函數可以讓咱們輕鬆的使用代理模式:ajax
var proxy = new Proxy(target, handler);
Proxy
構造函數傳入兩個參數,第一個參數target
表示所要代理的對象,第二個參數handler
也是一個對象用來設置對所代理的對象的行爲。若是想知道Proxy
的具體使用方法,可參考阮一峯的《 ECMAScript入門 - Proxy 》。編程
本文將利用Proxy
實現前端中3種代理模式的使用場景,分別是:緩存代理、驗證代理、實現私有屬性。後端
緩存代理能夠將一些開銷很大的方法的運算結果進行緩存,再次調用該函數時,若參數一致,則能夠直接返回緩存中的結果,而不用再從新進行運算。例如在採用後端分頁的表格時,每次頁碼改變時須要從新請求後端數據,咱們能夠將頁碼和對應結果進行緩存,當請求同一頁時就不用在進行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
Proxy
構造函數第二個參數中的set
方法,能夠很方便的驗證向一個對象的傳值。咱們以一個傳統的登錄表單舉例,該表單對象有兩個屬性,分別是account
和password
,每一個屬性值都有一個簡單和其屬性名對應的驗證方法,驗證規則以下:
// 表單對象 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的事件來實現頁面中的校驗提示。
代理模式還有一個很重要的應用是實現訪問限制。總所周知,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"
ES6提供的Proxy
可讓JS開發者很方便的使用代理模式,據說Vue 3.0的也會使用Proxy
去大量改寫核心代碼。雖然代理模式很方便,可是在業務開發時應該注意使用場景,不須要在編寫對象時就去預先猜想是否須要使用代理模式,只有當對象的功能變得複雜或者咱們須要進行必定的訪問限制時,再考慮使用代理。