前言
在ES6中,Proxy構造器是一種可訪問的全局對象,使用它你可以在對象與各種操作對象的行為之間收集有關請求操作的各種信息,並返回任何你想做的。 ES6中的箭頭函數、數組解構、rest 參數等特性一經實現就廣為流傳,但類似Proxy 這樣的特性卻很少見到有開發者在使用,一方面在於瀏覽器的兼容性,另一方面也在於要想發揮這些特性的優勢需要開發者深入地理解其使用場景。就我個人而言是非常喜歡ES6 的Proxy,因為它讓我們以簡潔易懂的方式控制了外部對對象的訪問。在下文中,首先我會介紹Proxy 的使用方式,然後列舉具體實例解釋Proxy 的使用場景。
Proxy ,見名知意,其功能非常類似於設計模式中的代理模式,該模式常用於三個方面:
1.和監視外部對對象的訪問
2.函數或類的複雜度
3.操作前對操作進行校驗或對所需資源進行管理
在支持Proxy 的瀏覽器環境中,Proxy 是一個全局對象,可以直接使用。 Proxy(target, handler)是一個構造函數, target是被代理的對象, handlder是聲明了各類代理操作的對象,最終返回一個代理對象。外界每次通過代理對象訪問target對象的屬性時,就會經過handler對象,從這個流程來看,代理對像很類似middleware(中間件)。那麼Proxy 可以攔截什麼操作呢?最常見的就是get(讀取)、set(修改)對象屬性等操作,完整的可攔截操作列表請點擊這裡。此外,Proxy 對像還提供了一個revoke方法,可以隨時註銷所有的代理操作。在我們正式介紹Proxy 之前,建議你對Reflect 有一定的了解,它也是一個ES6 新增的全局對象。
Basic
const target = { name: 'Billy Bob', age: 15};const handler = { get(target, key, proxy) { const today = new Date(); console.log(`GET request made for ${key} at ${today}`); return Reflect.get(target, key, proxy); }};const proxy = new Proxy(target, handler);proxy.name;// => "GET request made for name at Thu Jul 21 2016 15:26:20 GMT+0800 (CST)"// => "Billy Bob"在上面的代碼中,我們首先定義了一個被代理的目標對象target ,然後聲明了包含所有代理操作的handler對象,接下來使用Proxy(target, handler)創建代理對象proxy ,此後所有使用proxy對target屬性的訪問都會經過handler的處理。
1. 抽離校驗模塊
讓我們從一個簡單的類型校驗開始做起,這個示例演示瞭如何使用Proxy 保障數據類型的準確性:
let numericDataStore = { count: 0, amount: 1234, total: 14};numericDataStore = new Proxy(numericDataStore, { set(target, key, value, proxy) { if (typeof value !== 'number') { throw Error("Properties in numericDataStore can only be numbers"); } return Reflect.set(target, key, value, proxy); }});// 拋出錯誤,因為"foo" 不是數值numericDataStore.count = "foo";// 賦值成功numericDataStore.count = 333;如果要直接為對象的所有屬性開發一個校驗器可能很快就會讓代碼結構變得臃腫,使用Proxy 則可以將校驗器從核心邏輯分離出來自成一體:
function createValidator(target, validator) { return new Proxy(target, { _validator: validator, set(target, key, value, proxy) { if (target.hasOwnProperty(key)) { let validator = this._validator[key]; if (!!validator(value)) { return Reflect.set(target, key, value, proxy); } else { throw Error(`Cannot set ${key} to ${value}. Invalid.`); } } else { throw Error(`${key} is not a valid property`) } } });}const personValidators = { name(val) { return typeof val === 'string'; }, age(val) { return typeof age === 'number' && age > 18; }}class Person { constructor(name, age) { this.name = name; this.age = age; return createValidator(this, personValidators); }}const bill = new Person('Bill', 25);// 以下操作都會報錯bill.name = 0; bill.age = 'Bill'; bill.age = 15;通過校驗器和主邏輯的分離,你可以無限擴展personValidators校驗器的內容,而不會對相關的類或函數造成直接破壞。更複雜一點,我們還可以使用Proxy 模擬類型檢查,檢查函數是否接收了類型和數量都正確的參數:
let obj = { pickyMethodOne: function(obj, str, num) { /* ... */ }, pickyMethodTwo: function(num, obj) { /*... */ }};const argTypes = { pickyMethodOne: ["object", "string", "number"], pickyMethodTwo: ["number", "object"]};obj = new Proxy(obj, { get: function(target, key, proxy) { var value = target[key]; return function(...args) { var checkArgs = argChecker(key, args, argTypes[key]); return Reflect.apply(value, target, args); }; }});function argChecker(name, args, checkers) { for (var idx = 0; idx < args.length; idx++) { var arg = args[idx]; var type = checkers[idx]; if (!arg || typeof arg !== type) { console.warn(`You are incorrectly implementing the signature of ${name}. Check param ${idx + 1}`); } }}obj.pickyMethodOne(); // > You are incorrectly implementing the signature of pickyMethodOne. Check param 1// > You are incorrectly implementing the signature of pickyMethodOne. Check param 2// > You are incorrectly implementing the signature of pickyMethodOne. Check param 3obj.pickyMethodTwo("wopdopadoo", {}); // > You are incorrectly implementing the signature of pickyMethodTwo. Check param 1// No warnings loggedobj.pickyMethodOne({}, "a little string", 123); obj.pickyMethodOne(123, {});2. 私有屬性
在JavaScript 或其他語言中,大家會約定俗成地在變量名之前添加下劃線_來表明這是一個私有屬性(並不是真正的私有),但我們無法保證真的沒人會去訪問或修改它。在下面的代碼中,我們聲明了一個私有的apiKey ,便於api這個對象內部的方法調用,但不希望從外部也能夠訪問api._apiKey :
var api = { _apiKey: '123abc456def', /* mock methods that use this._apiKey */ getUsers: function(){}, getUser: function(userId){}, setUser: function(userId, config){}};// logs '123abc456def';console.log("An apiKey we want to keep private", api._apiKey);// get and mutate _apiKeys as desiredvar apiKey = api._apiKey; api._apiKey = '987654321';很顯然,約定俗成是沒有束縛力的。使用ES6 Proxy 我們就可以實現真實的私有變量了,下面針對不同的讀取方式演示兩個不同的私有化方法。
第一種方法是使用set / get 攔截讀寫請求並返回undefined:
let api = { _apiKey: '123abc456def', getUsers: function(){ }, getUser: function(userId){ }, setUser: function(userId, config){ }};const RESTRICTED = ['_apiKey'];api = new Proxy(api, { get(target, key, proxy) { if(RESTRICTED.indexOf(key) > -1) { throw Error(`${key} is restricted. Please see api documentation for further info.`); } return Reflect.get(target, key, proxy); }, set(target, key, value, proxy) { if(RESTRICTED.indexOf(key) > -1) { throw Error(`${key} is restricted. Please see api documentation for further info.`); } return Reflect.get(target, key, value, proxy); }});// 以下操作都會拋出錯誤console.log(api._apiKey);api._apiKey = '987654321';第二種方法是使用has 攔截in 操作:
var api = { _apiKey: '123abc456def', getUsers: function(){ }, getUser: function(userId){ }, setUser: function(userId, config){ }};const RESTRICTED = ['_apiKey'];api = new Proxy(api, { has(target, key) { return (RESTRICTED.indexOf(key) > -1) ? false : Reflect.has(target, key); }});// these log false, and `for in` iterators will ignore _apiKeyconsole.log("_apiKey" in api);for (var key in api) { if (api.hasOwnProperty(key) && key === "_apiKey") { console.log("This will never be logged because the proxy obscures _apiKey...") }}3. 訪問日誌
對於那些調用頻繁、運行緩慢或占用執行環境資源較多的屬性或接口,開發者會希望記錄它們的使用情況或性能表現,這個時候就可以使用Proxy 充當中間件的角色,輕而易舉實現日誌功能:
let api = { _apiKey: '123abc456def', getUsers: function() { /* ... */ }, getUser: function(userId) { /* ... */ }, setUser: function(userId, config) { /* ... */ }};function logMethodAsync(timestamp, method) { setTimeout(function() { console.log(`${timestamp} - Logging ${method} request asynchronously.`); }, 0)}api = new Proxy(api, { get: function(target, key, proxy) { var value = target[key]; return function(...arguments) { logMethodAsync(new Date(), key); return Reflect.apply(value, target, arguments); }; }});api.getUsers();4. 預警和攔截
假設你不想讓其他開發者刪除noDelete屬性,還想讓調用oldMethod的開發者了解到這個方法已經被廢棄了,或者告訴開發者不要修改doNotChange屬性,那麼就可以使用Proxy 來實現:
let dataStore = { noDelete: 1235, oldMethod: function() {/*...*/ }, doNotChange: "tried and true"};const NODELETE = ['noDelete']; const NOCHANGE = ['doNotChange'];const DEPRECATED = ['oldMethod']; dataStore = new Proxy(dataStore, { set(target, key, value, proxy) { if (NOCHANGE.includes(key)) { throw Error(`Error! ${key} is immutable.`); } return Reflect.set(target, key, value, proxy); }, deleteProperty(target, key) { if (NODELETE.includes(key)) { throw Error(`Error! ${key} cannot be deleted.`); } return Reflect.deleteProperty(target, key); }, get(target, key, proxy) { if (DEPRECATED.includes(key)) { console.warn(`Warning! ${key} is deprecated.`); } var val = target[key]; return typeof val === 'function' ? function(...args) { Reflect.apply(target[key], target, args); } : val; }});// these will throw errors or log warnings, respectivelydataStore.doNotChange = "foo"; delete dataStore.noDelete; dataStore.oldMethod();5. 過濾操作
某些操作會非常佔用資源,比如傳輸大文件,這個時候如果文件已經在分塊發送了,就不需要在對新的請求作出相應(非絕對),這個時候就可以使用Proxy 對當請求進行特徵檢測,並根據特徵過濾出哪些是不需要響應的,哪些是需要響應的。下面的代碼簡單演示了過濾特徵的方式,並不是完整代碼,相信大家會理解其中的妙處:
let obj = { getGiantFile: function(fileId) {/*...*/ }};obj = new Proxy(obj, { get(target, key, proxy) { return function(...args) { const id = args[0]; let isEnroute = checkEnroute(id); let isDownloading = checkStatus(id); let cached = getCached(id); if (isEnroute || isDownloading) { return false; } if (cached) { return cached; } return Reflect.apply(target[key], target, args); } }});6. 中斷代理
Proxy 支持隨時取消對target的代理,這一操作常用於完全封閉對數據或接口的訪問。在下面的示例中,我們使用了Proxy.revocable方法創建了可撤銷代理的代理對象:
let sensitiveData = { username: 'devbryce' };const {sensitiveData, revokeAccess} = Proxy.revocable(sensitiveData, handler);function handleSuspectedHack(){ revokeAccess();}// logs 'devbryce'console.log(sensitiveData.username);handleSuspectedHack();// TypeError: Revokedconsole.log(sensitiveData.username);Decorator
ES7 中實現的Decorator,相當於設計模式中的裝飾器模式。如果簡單地區分Proxy和Decorator的使用場景,可以概括為:Proxy 的核心作用是控制外界對被代理者內部的訪問,Decorator 的核心作用是增強被裝飾者的功能。只要在它們核心的使用場景上做好區別,那麼像是訪問日誌這樣的功能,雖然本文使用了Proxy 實現,但也可以使用Decorator 實現,開發者可以根據項目的需求、團隊的規範、自己的偏好自由選擇。
總結
ES6 的Proxy還是非常實用的,看似簡單的特性,卻有極大的用處。希望給大家學習ES6有所幫助。