一环础、proxy概述
Proxy的兼容性
proxy
在目標(biāo)對象的外層搭建了一層攔截,外界對目標(biāo)對象的某些操作载慈,必須通過這層攔截
var proxy = new Proxy(target, handler);
new Proxy()
表示生成一個Proxy
實例缸匪,target
參數(shù)表示所要攔截的目標(biāo)對象,handler
參數(shù)也是一個對象准夷,用來定制攔截行為
var target = {
name: 'poetries'
};
var logHandler = {
get: function(target, key) {
console.log(`${key} 被讀取`);
return target[key];
},
set: function(target, key, value) {
console.log(`${key} 被設(shè)置為 ${value}`);
target[key] = value;
}
}
var targetWithLog = new Proxy(target, logHandler);
targetWithLog.name; // 控制臺輸出:name 被讀取
targetWithLog.name = 'others'; // 控制臺輸出:name 被設(shè)置為 others
console.log(target.name); // 控制臺輸出: others
-
targetWithLog
讀取屬性的值時钥飞,實際上執(zhí)行的是logHandler.get
:在控制臺輸出信息,并且讀取被代理對象target
的屬性衫嵌。 - 在
targetWithLog
設(shè)置屬性值時读宙,實際上執(zhí)行的是logHandler.set
:在控制臺輸出信息,并且設(shè)置被代理對象target
的屬性的值
// 由于攔截函數(shù)總是返回35楔绞,所以訪問任何屬性都得到35
var proxy = new Proxy({}, {
get: function(target, property) {
return 35;
}
});
proxy.time // 35
proxy.name // 35
proxy.title // 35
Proxy 實例也可以作為其他對象的原型對象
var proxy = new Proxy({}, {
get: function(target, property) {
return 35;
}
});
let obj = Object.create(proxy);
obj.time // 35
proxy
對象是obj
對象的原型结闸,obj
對象本身并沒有time
屬性,所以根據(jù)原型鏈酒朵,會在proxy
對象上讀取該屬性桦锄,導(dǎo)致被攔截
Proxy的作用
對于代理模式
Proxy
的作用主要體現(xiàn)在三個方面
- 攔截和監(jiān)視外部對對象的訪問
- 降低函數(shù)或類的復(fù)雜度
- 在復(fù)雜操作前對操作進(jìn)行校驗或?qū)λ栀Y源進(jìn)行管理
二、Proxy所能代理的范圍--handler
實際上
handler
本身就是ES6
所新設(shè)計的一個對象.它的作用就是用來 自定義代理對象的各種可代理操作 耻讽。它本身一共有13
中方法,每種方法都可以代理一種操作.其13
種方法如下
// 在讀取代理對象的原型時觸發(fā)該操作察纯,比如在執(zhí)行 Object.getPrototypeOf(proxy) 時。
handler.getPrototypeOf()
// 在設(shè)置代理對象的原型時觸發(fā)該操作,比如在執(zhí)行 Object.setPrototypeOf(proxy, null) 時饼记。
handler.setPrototypeOf()
// 在判斷一個代理對象是否是可擴展時觸發(fā)該操作香伴,比如在執(zhí)行 Object.isExtensible(proxy) 時。
handler.isExtensible()
// 在讓一個代理對象不可擴展時觸發(fā)該操作具则,比如在執(zhí)行 Object.preventExtensions(proxy) 時即纲。
handler.preventExtensions()
// 在獲取代理對象某個屬性的屬性描述時觸發(fā)該操作,比如在執(zhí)行 Object.getOwnPropertyDescriptor(proxy, "foo") 時博肋。
handler.getOwnPropertyDescriptor()
// 在定義代理對象某個屬性時的屬性描述時觸發(fā)該操作低斋,比如在執(zhí)行 Object.defineProperty(proxy, "foo", {}) 時。
andler.defineProperty()
// 在判斷代理對象是否擁有某個屬性時觸發(fā)該操作匪凡,比如在執(zhí)行 "foo" in proxy 時膊畴。
handler.has()
// 在讀取代理對象的某個屬性時觸發(fā)該操作,比如在執(zhí)行 proxy.foo 時病游。
handler.get()
// 在給代理對象的某個屬性賦值時觸發(fā)該操作唇跨,比如在執(zhí)行 proxy.foo = 1 時。
handler.set()
// 在刪除代理對象的某個屬性時觸發(fā)該操作衬衬,比如在執(zhí)行 delete proxy.foo 時买猖。
handler.deleteProperty()
// 在獲取代理對象的所有屬性鍵時觸發(fā)該操作,比如在執(zhí)行 Object.getOwnPropertyNames(proxy) 時滋尉。
handler.ownKeys()
// 在調(diào)用一個目標(biāo)對象為函數(shù)的代理對象時觸發(fā)該操作玉控,比如在執(zhí)行 proxy() 時。
handler.apply()
// 在給一個目標(biāo)對象為構(gòu)造函數(shù)的代理對象構(gòu)造實例時觸發(fā)該操作狮惜,比如在執(zhí)行new proxy() 時高诺。
handler.construct()
三、Proxy場景
3.1 實現(xiàn)私有變量
var target = {
name: 'poetries',
_age: 22
}
var logHandler = {
get: function(target,key){
if(key.startsWith('_')){
console.log('私有變量age不能被訪問')
return false
}
return target[key];
},
set: function(target, key, value) {
if(key.startsWith('_')){
console.log('私有變量age不能被修改')
return false
}
target[key] = value;
}
}
var targetWithLog = new Proxy(target, logHandler);
// 私有變量age不能被訪問
targetWithLog.name;
// 私有變量age不能被修改
targetWithLog.name = 'others';
在下面的代碼中讽挟,我們聲明了一個私有的
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.2 抽離校驗?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' && val > 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, {});
3.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();
3.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();
3.5 過濾操作
某些操作會非常占用資源而叼,比如傳輸大文件,這個時候如果文件已經(jīng)在分塊發(fā)送了豹悬,就不需要在對新的請求作出相應(yīng)(非絕對)葵陵,這個時候就可以使用
Proxy
對當(dāng)請求進(jìn)行特征檢測,并根據(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);
}
}
});
3.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);