實例解析ES6 Proxy使用場景介紹
ES6 中的箭頭函數(shù)、數(shù)組解構(gòu)、rest 參數(shù)等特性一經(jīng)實現(xiàn)就廣為流傳,但類似 Proxy 這樣的特性卻很少見到有開發(fā)者在使用,一方面在于瀏覽器的兼容性,另一方面也在于要想發(fā)揮這些特性的優(yōu)勢需要開發(fā)者深入地理解其使用場景。就我個人而言是非常喜歡 ES6 的 Proxy,因為它讓我們以簡潔易懂的方式控制了外部對對象的訪問。在下文中,首先我會介紹 Proxy 的使用方式,然后列舉具體實例解釋 Proxy 的使用場景。
Proxy,見名知意,其功能非常類似于設(shè)計模式中的代理模式,該模式常用于三個方面:
- 攔截和監(jiān)視外部對對象的訪問
- 降低函數(shù)或類的復(fù)雜度
- 在復(fù)雜操作前對操作進行校驗或?qū)λ栀Y源進行管理
在支持 Proxy 的瀏覽器環(huán)境中,Proxy 是一個全局對象,可以直接使用。Proxy(target, handler) 是一個構(gòu)造函數(shù),target 是被代理的對象,handlder 是聲明了各類代理操作的對象,最終返回一個代理對象。外界每次通過代理對象訪問 target 對象的屬性時,就會經(jīng)過 handler 對象,從這個流程來看,代理對象很類似 middleware(中間件)。那么 Proxy 可以攔截什么操作呢?最常見的就是 get(讀?。?、set(修改)對象屬性等操作,完整的可攔截操作列表請點擊這里。此外,Proxy 對象還提供了一個 revoke 方法,可以隨時注銷所有的代理操作。在我們正式介紹 Proxy 之前,建議你對 Reflect 有一定的了解,它也是一個 ES6 新增的全局對象,詳細(xì)信息請參考MDN Reflect。
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"
在上面的代碼中,我們首先定義了一個被代理的目標(biāo)對象 target,然后聲明了包含所有代理操作的 handler 對象,接下來使用 Proxy(target, handler) 創(chuàng)建代理對象 proxy,此后所有使用 proxy 對 target 屬性的訪問都會經(jīng)過 handler 的處理。
1. 抽離校驗?zāi)K
讓我們從一個簡單的類型校驗開始做起,這個示例演示了如何使用 Proxy 保障數(shù)據(jù)類型的準(zhǔn)確性:
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" 不是數(shù)值 numericDataStore.count = "foo"; // 賦值成功 numericDataStore.count = 333;
如果要直接為對象的所有屬性開發(fā)一個校驗器可能很快就會讓代碼結(jié)構(gòu)變得臃腫,使用 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 校驗器的內(nèi)容,而不會對相關(guān)的類或函數(shù)造成直接破壞。更復(fù)雜一點,我們還可以使用 Proxy 模擬類型檢查,檢查函數(shù)是否接收了類型和數(shù)量都正確的參數(shù):
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 3 obj.pickyMethodTwo("wopdopadoo", {}); // > You are incorrectly implementing the signature of pickyMethodTwo. Check param 1 // No warnings logged obj.pickyMethodOne({}, "a little string", 123); obj.pickyMethodOne(123, {});
2. 私有屬性
在 JavaScript 或其他語言中,大家會約定俗成地在變量名之前添加下劃線 _ 來表明這是一個私有屬性(并不是真正的私有),但我們無法保證真的沒人會去訪問或修改它。在下面的代碼中,我們聲明了一個私有的 apiKey,便于 api 這個對象內(nèi)部的方法調(diào)用,但不希望從外部也能夠訪問 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 desired var apiKey = api._apiKey; api._apiKey = '987654321';
很顯然,約定俗成是沒有束縛力的。使用 ES6 Proxy 我們就可以實現(xiàn)真實的私有變量了,下面針對不同的讀取方式演示兩個不同的私有化方法。第一種方法是使用 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 _apiKey console.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. 訪問日志
對于那些調(diào)用頻繁、運行緩慢或占用執(zhí)行環(huán)境資源較多的屬性或接口,開發(fā)者會希望記錄它們的使用情況或性能表現(xiàn),這個時候就可以使用 Proxy 充當(dāng)中間件的角色,輕而易舉實現(xiàn)日志功能:
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. 預(yù)警和攔截
假設(shè)你不想讓其他開發(fā)者刪除 noDelete 屬性,還想讓調(diào)用 oldMethod 的開發(fā)者了解到這個方法已經(jīng)被廢棄了,或者告訴開發(fā)者不要修改 doNotChange 屬性,那么就可以使用 Proxy 來實現(xiàn):
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, respectively dataStore.doNotChange = "foo"; delete dataStore.noDelete; dataStore.oldMethod();
5. 過濾操作
某些操作會非常占用資源,比如傳輸大文件,這個時候如果文件已經(jīng)在分塊發(fā)送了,就不需要在對新的請求作出相應(yīng)(非絕對),這個時候就可以使用 Proxy 對當(dāng)請求進行特征檢測,并根據(jù)特征過濾出哪些是不需要響應(yīng)的,哪些是需要響應(yīng)的。下面的代碼簡單演示了過濾特征的方式,并不是完整代碼,相信大家會理解其中的妙處:
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 的代理,這一操作常用于完全封閉對數(shù)據(jù)或接口的訪問。在下面的示例中,我們使用了 Proxy.revocable 方法創(chuàng)建了可撤銷代理的代理對象:
let sensitiveData = { username: 'devbryce' }; const {sensitiveData, revokeAccess} = Proxy.revocable(sensitiveData, handler); function handleSuspectedHack(){ revokeAccess(); } // logs 'devbryce' console.log(sensitiveData.username); handleSuspectedHack(); // TypeError: Revoked console.log(sensitiveData.username);
Decorator
ES7 中實現(xiàn)的 Decorator,相當(dāng)于設(shè)計模式中的裝飾器模式。如果簡單地區(qū)分 Proxy 和 Decorator 的使用場景,可以概括為:Proxy 的核心作用是控制外界對被代理者內(nèi)部的訪問,Decorator 的核心作用是增強被裝飾者的功能。只要在它們核心的使用場景上做好區(qū)別,那么像是訪問日志這樣的功能,雖然本文使用了 Proxy 實現(xiàn),但也可以使用 Decorator 實現(xiàn),開發(fā)者可以根據(jù)項目的需求、團隊的規(guī)范、自己的偏好自由選擇。
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- 詳解ES6中的代理模式——Proxy
- ES6中Proxy與Reflect實現(xiàn)重載(overload)的方法
- 詳細(xì)探究ES6之Proxy代理
- ES6中Proxy代理用法實例淺析
- 淺談es6語法 (Proxy和Reflect的對比)
- ES6之Proxy的get方法詳解
- JavaScript中的ES6 Proxy的具體使用
- ES6 Proxy實現(xiàn)Vue的變化檢測問題
- ES6知識點整理之Proxy的應(yīng)用實例詳解
- ES6 proxy和reflect的使用方法與應(yīng)用實例分析
- ES6中javascript實現(xiàn)函數(shù)綁定及類的事件綁定功能詳解
- ES6使用新特性Proxy實現(xiàn)的數(shù)據(jù)綁定功能實例
相關(guān)文章
JS 判斷某變量是否為某數(shù)組中的一個值的3種方法(總結(jié))
下面小編就為大家?guī)硪黄狫S 判斷某變量是否為某數(shù)組中的一個值的3種方法(總結(jié))。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-07-07微信小程序頁面?zhèn)鞫鄠€參數(shù)跳轉(zhuǎn)頁面的實現(xiàn)方法
這篇文章主要介紹了微信小程序頁面?zhèn)鞫鄠€參數(shù)跳轉(zhuǎn)頁面的實現(xiàn)方法,本文圖文并茂給大家介紹的非常詳細(xì),具有一定的參考借鑒價值,需要的朋友可以參考下2019-05-05javascript實現(xiàn)點擊后變換按鈕顯示文字的方法
這篇文章主要介紹了javascript實現(xiàn)點擊后變換按鈕顯示文字的方法,可實現(xiàn)顯示一些按鈕如果點擊了,按鈕文本變?yōu)椤包c了”,其他按鈕文本變?yōu)椤皼]點”的效果,非常具有實用價值,需要的朋友可以參考下2015-05-05IE6/IE7中JavaScript json提示缺少標(biāo)識符、字符串或數(shù)字問題處理
這篇文章主要介紹了IE6/IE7中JavaScript json提示缺少標(biāo)識符、字符串或數(shù)字問題處理,需要的朋友可以參考下2014-12-12javascript運動效果實例總結(jié)(放大縮小、滑動淡入、滾動)
這篇文章主要介紹了javascript運動效果,結(jié)合實例形式總結(jié)分析JavaScript實現(xiàn)放大縮小、滑動淡入、滾動等效果的方法,需要的朋友可以參考下2016-01-01