一,基礎
1. 概念
對外界的訪問進行過濾和改寫
2. 基本用法
// 用法1
var object = { proxy: new Proxy(target, handler) };
// 用法2
var proxy = new Proxy({}, {
get: function(target, propKey) {
return 35;
}
});
let obj = Object.create(proxy);
obj.time // 35
3. 攔截操作
- get(target, propKey, receiver):攔截對象屬性的讀取,比如proxy.foo和proxy[‘foo’]。
應用1:訪問不存在的屬性返回undefined, 設置報錯機制
var person = {
name: "張三"
};
var proxy = new Proxy(person, {
get: function(target, propKey) {
if (Reflect.has(target, prokey)) {
return target[propKey];
} else {
throw new ReferenceError("Prop name \"" + propKey + "\" does not exist.");
}
}
});
proxy.name // "張三"
proxy.age // 拋出一個錯誤
應用2: get
方法可以繼承。
let proto = new Proxy({}, {
get(target, propertyKey, receiver) {
console.log('GET ' + propertyKey);
return target[propertyKey];
}
});
let obj = Object.create(proto);
obj.foo // "GET foo"
應用3: 實現屬性的鏈式操作。
var pipe = function(value) {
var funcStacks = [] // 函數棧
var oproxy = new Proxy({}, {
get: function(pipeObject, fnName) {// pipeObject={},代理對象爲空,fnName爲設置的屬性值double,pow,reverseInt,get
if( fnName == 'get') { // 不到最後的get不執行調用函數
return funcStacks.reduce(function(val, fn){
console.log(typeof fn)
console.log('coloe:'+fn)
if(typeof fn === 'function') {
// 調用函數並返回結果
return fn(val)
}
}, value) // 傳入外部的值
}
// 每次調用都將該函數放到函數棧裏面待機。
funcStacks.push(window[fnName])
console.log(funcStacks)
// 返回代理權繼續操作
return oproxy
}
})
return oproxy
}
var double = n => n * 2
var pow = n => n * n
var reverseInt = n => n.toString().split("").reverse().join("") | 0
pipe(3).double.pow.reverseInt.get; // 63
- set(target, propKey, value, receiver):攔截對象屬性的設置,比如proxy.foo = v或proxy[‘foo’] = v,返回一個布爾值。
- apply(target, object, args):攔截 Proxy 實例作爲函數調用的操作,比如proxy(…args)、proxy.call(object, …args)、proxy.apply(…)。
apply
方法可以接受三個參數,分別是目標對象、目標對象的上下文對象(this
)和目標對象的參數數組。
var handler = {
apply (target, ctx, args) {
return Reflect.apply(...arguments);
}
};
- has(target, propKey):攔截propKey in proxy的操作,返回一個布爾值。
has
方法用來攔截HasProperty
操作,即判斷對象是否具有某個屬性時,這個方法會生效。典型的操作就是in
運算符。
has
方法可以接受兩個參數,分別是目標對象、需查詢的屬性名。
-
使用
has
方法隱藏某些屬性,不被in
運算符發現。 -
has
方法攔截的是HasProperty
操作,而不是HasOwnProperty
操作,即has
方法不判斷一個屬性是對象自身的屬性,還是繼承的屬性。 -
對
for...in
循環不生效 -
for...in
和hasOwnProperty
使用ownKeys
攔截
- construct(target, args):攔截 Proxy 實例作爲構造函數調用的操作,比如new proxy(…args)。
construct
方法用於攔截new
命令,下面是攔截對象的寫法。
var handler = {
construct (target, args, newTarget) {
return new target(...args);
}
};
construct
方法可以接受三個參數。
target
:目標對象args
:構造函數的參數對象newTarget
:創造實例對象時,new
命令作用的構造函數(下面例子的p
)
返回值:返回的必須是一個對象,否則會報錯
- deleteProperty(target, propKey):攔截delete proxy[propKey]的操作,返回一個布爾值。
攔截delete
操作,如果這個方法拋出錯誤或者返回false
,當前屬性就無法被delete
命令刪除。
- defineProperty(target, propKey, propDesc):攔截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一個布爾值。
- getOwnPropertyDescriptor(target, propKey):攔截Object.getOwnPropertyDescriptor(proxy, propKey),返回屬性的描述對象。
返回一個屬性描述對象或者undefined
。
- getPrototypeOf(target):攔截Object.getPrototypeOf(proxy),返回一個對象。
攔截獲取對象原型
返回值必須是對象或者null
,否則報錯
Object.prototype.__proto__
Object.prototype.isPrototypeOf()
Object.getPrototypeOf()
Reflect.getPrototypeOf()
instanceof
- isExtensible(target):攔截Object.isExtensible(proxy),返回一個布爾值。
只能返回布爾值,否則返回值會被自動轉爲布爾值
**強限制:**它的返回值必須與目標對象的isExtensible
屬性保持一致,否則就會拋出錯誤。
- ownKeys(target):攔截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for…in循環,返回一個數組。該方法返回目標對象所有自身的屬性的屬性名,而Object.keys()的返回結果僅包括目標對象自身的可遍歷屬性。
攔截對象自身屬性的讀取操作
Object.getOwnPropertyNames()
Object.getOwnPropertySymbols()
Object.keys()
for...in
循環
有三類屬性會被ownKeys()
方法自動過濾,不會返回。
- 目標對象上不存在的屬性
- 屬性名爲 Symbol 值
- 不可遍歷(
enumerable
)的屬性
- preventExtensions(target):攔截Object.preventExtensions(proxy),返回一個布爾值。
只有目標對象不可擴展時(即Object.isExtensible(proxy)
爲false
),proxy.preventExtensions
才能返回true
,否則會報錯。
- setPrototypeOf(target, proto):攔截Object.setPrototypeOf(proxy, proto),返回一個布爾值。如果目標對象是函數,那麼還有兩種額外操作可以攔截。
只能返回布爾值,否則會被自動轉爲布爾值
- revocable(target, handler): 取消proxy實例
示例看下面的撤銷代理
通用情景整理
- 第三個參數
receiver
總是指向原始的讀操作所在的那個對象,一般情況下就是 Proxy 實例
const proxy = new Proxy({}, {
get: function(target, key, receiver) {
return receiver;
},
set: function(obj, prop, value, receiver) {
obj[prop] = receiver;
}
});
// get
proxy.getReceiver === proxy // true
const d = Object.create(proxy);
d.a === d // true
// set
const proxy = new Proxy({}, handler);
proxy.foo = 'bar';
proxy.foo === proxy // true
- 如果一個屬性不可配置(configurable)且不可寫(writable),則 Proxy 不能修改該屬性,否則會報錯
const target = Object.defineProperties({}, {
foo: {
value: 123,
writable: false,
configurable: false
},
});
// 不可擴展
Object.preventExtensions(target);
const handler = {
// 如果一個屬性不可配置(configurable)且不可寫(writable),則 Proxy 不能修改該屬性,否則通過 Proxy 對象訪問該屬性會報錯
get(target, propKey) {
return 'abc';
},
// 如果目標對象自身的某個屬性,不可寫且不可配置,那麼set方法將不起作用。
set: function(obj, prop, value, receiver) {
// 嚴格模式下,set代理如果沒有返回true,就會報錯。
obj[prop] = 'baz';
},
// 如果原對象不可配置或者禁止擴展,這時`has`攔截會報錯。
has: function(target, prop) {
return false;
},
// 目標對象自身的不可配置(configurable)的屬性,不能被deleteProperty方法刪除,否則報錯。
deleteProperty (target, key) {
invariant(key, 'delete');
delete target[key];
return true;
},
// 如果目標對象不可擴展(non-extensible),則不能增加目標對象上不存在的屬性,否則會報錯。
// 如果目標對象的某個屬性不可寫(writable)或不可配置(configurable),則不得改變這兩個設置。
defineProperty (target, key, descriptor) {
return false; // false只是用來提示操作失敗,本身並不能阻止添加新屬性。
},
// 如果目標對象不可擴展(non-extensible), getPrototypeOf()方法必須返回目標對象的原型對象。
getPrototypeOf(target) {
return target.prototype;
},
// 如果目標對象自身包含不可配置的屬性,則該屬性必須被`ownKeys()`方法返回,否則報錯。
// 如果目標對象是不可擴展的(non-extensible),這時ownKeys()方法返回的數組之中,必須包含原對象的所有屬性,且不能包含多餘的屬性,否則報錯。
ownKeys: function(target) {
return ['foo'];
},
// 只有目標對象不可擴展時(即Object.isExtensible(proxy)爲false),proxy.preventExtensions才能返回true,否則會報錯。
preventExtensions: function(target) {
console.log('called');
Object.preventExtensions(target);
return true;
},
// 如果目標對象不可擴展(non-extensible),setPrototypeOf()方法不得改變目標對象的原型。
setPrototypeOf (target, proto) {
throw new Error('Changing the prototype is forbidden');
}
};
const proxy = new Proxy(target, handler);
// get
proxy.foo
// set
proxy.foo = 'baz';
proxy.foo // 123
// has
'a' in p // TypeError is thrown
// ownKeys
Object.getOwnPropertyNames(p)
// preventExtensions
Object.preventExtensions(proxy)
// "called"
// Proxy {}
// setprototypeOf 不可修改對象原型
var proto = {};
var target = function () {};
var proxy = new Proxy(target, handler);
Object.setPrototypeOf(proxy, proto);
// Error: Changing the prototype is forbidden
- 防止內部屬性被外部讀寫。
const handler = {
get (target, key) {
invariant(key, 'get');
return target[key];
},
set (target, key, value) {
invariant(key, 'set');
target[key] = value;
return true;
},
deleteProperty (target, key) {
invariant(key, 'delete');
delete target[key];
return true;
},
getOwnPropertyDescriptor (target, key) {
if (key[0] === '_') {
return;
}
return Object.getOwnPropertyDescriptor(target, key);
},
ownKeys (target) {
return Reflect.ownKeys(target).filter(key => key[0] !== '_');
}
};
function invariant (key, action) {
if (key[0] === '_') {
throw new Error(`Invalid attempt to ${action} private "${key}" property`);
}
}
var target = {};
var proxy = new Proxy(target, handler);
proxy._prop
// Error: Invalid attempt to get private "_prop" property
proxy._prop = 'c'
// Error: Invalid attempt to set private "_prop" property
// getOwnPropertyDescriptor
target = { _foo: 'bar', baz: 'tar' };
proxy = new Proxy(target, handler);
Object.getOwnPropertyDescriptor(proxy, 'wat')
// undefined
Object.getOwnPropertyDescriptor(proxy, '_foo')
// undefined
Object.getOwnPropertyDescriptor(proxy, 'baz')
// { value: 'tar', writable: true, enumerable: true, configurable: true }
// ownKeys
target = {
_bar: 'foo',
_prop: 'bar',
prop: 'baz'
};
proxy = new Proxy(target, handler);
for (let key of Object.keys(proxy)) {
console.log(target[key]);
}
- this指向問題
目標對象內部的this
關鍵字會指向 Proxy 代理。
const target = new Date('2015-01-01');
const handler = {
get(target, prop) {
if (prop === 'getDate') {
// 給原來的對象綁定this,不然找不到原對象屬性
return target.getDate.bind(target);
}
return Reflect.get(target, prop);
}
};
const proxy = new Proxy(target, handler);
proxy.getDate() // 1
二,應用
1. 私有化api
let api = {
_apiKey: '123abc456def',
getUsers: function(){ },
getUser: function(userId){ },
setUser: function(userId, config){ }
};
console.log(api._apiKey); // 此處可以使用
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';
2. 生成隨機編碼,只讀
class Component {
constructor () {
this.proxy = new Proxy({
id: Math.random().toString(36).slice(-8)
},{})
}
get id() {
return this.proxy.id
}
}
let com = new Component()
let com2 = new Component()
for(let i = 0; i < 10; i++){
console.log(com.id,com2.id)
}
com.id = '123' // 設置無效,只讀
console.log(com.id,com2.id)
3. 校驗表單
簡單校驗
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;
抽離邏輯校驗
function createValidator(target, validator) {
return new Proxy(target, {
_validator: validator,
set(target, key, value, proxy) {
if (Reflect.has(target, 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;
擴展校驗器,增加校驗類型
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, {});
4. 請求時間日誌,分析性能
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();
5. 預警和攔截
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();
6. 過濾操作
某些操作會非常佔用資源,比如傳輸大文件,這個時候如果文件已經在分塊發送了,就不需要在對新的請求作出相應(非絕對),這個時候就可以使用 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);
}
}
});
7. 撤銷代理
let o = {
name: 'xiaoming',
price : 190
}
let d = Proxy.revocable(o, {
get (target, key) {
if(key === 'price') {
return target[key] + 20
}else {
return target[key]
}
}
})
console.log(d.proxy.price, d)
setTimeout(function () {
d.revoke()
setTimeout(function () {
console.log(d.proxy.price)
},100)
},1000)
8. 實現觀察者模式
const queuedObservers = new Set() // 觀察者隊列
const observe = fn => queuedObservers.add(fn) // 添加觀察者函數
const observable = obj => new Proxy(obj, {set, get}) // 代理觀察者
function set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver) // 重置原屬性,已經替換了原來的屬性值
queuedObservers.forEach(observer => observer()) // 執行觀察者函數
// return Reflect.set(target, key, value, receiver) // 如果是後面賦值,最後也會改變屬性值,但是在觀察者函數中,該屬性值還是原來的屬性值。我們需要對新的值處理,所以是之前就要賦值再進行觀察處理。
return result
}
function get(target, key, value, receiver) {
return Reflect.get(target, key, value, receiver)
}
let user = {
name: '張三',
age: 28
}
const person = observable(user)
let gcUser = () => {
// 打印新屬性
console.log(`${person.name}, ${person.age}`)
}
observe(gcUser)
person.name = '李四' // 李四, 28
9. 服務端代理,跨域,取消請求等
function createWebService(baseUrl) {
return new Proxy({}, {
get(target, propKey, receiver) {
return () => httpGet(baseUrl + '/' + propKey);
}
... // 各種情況處理
});
}