JS 基础:proxy
proxy
在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。文档查阅
Proxy
是一种代理模式,常用于三个方面:
- 拦截和监视外部对对象的访问
- 降低函数或类的复杂度
- 在复杂操作前对操作进行校验或对所需资源进行管理
正因为此,可以做一些中间件
相关的事情。
抽离校验模块
js
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();
obj.pickyMethodTwo('wopdopadoo', {});
obj.pickyMethodOne({}, 'a little string', 123);
obj.pickyMethodOne(123, {});
私有属性
js
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';
访问日志
js
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();
预警和拦截
js
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();
过滤操作
js
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);
};
},
});
中断代理
js
let data = {
username: 'korey',
};
const { proxy, revoke } = Proxy.revocable(data, {});
// logs 'devbryce'
console.log(proxy.username);
revoke();
// TypeError: Revoked
console.log(proxy.username);
解决对象属性为 undefined 的问题
js
(() => {
let target = {};
let handlers = {
get: (target, property) => {
target[property] = property in target ? target[property] : {};
if (typeof target[property] === 'object') {
return new Proxy(target[property], handlers);
}
return target[property];
},
};
let proxy = new Proxy(target, handlers);
console.log('z' in proxy.x.y); // false (其实这一步已经针对`target`创建了一个x.y的属性)
proxy.x.y.z = 'hello';
console.log('z' in proxy.x.y); // true
console.log(target.x.y.z); // hello
})();
普通函数与构造函数的兼容处理
js
class Test {
constructor(a, b) {
console.log('constructor', a, b);
}
}
// Test(1, 2) // throw an error
let proxyClass = new Proxy(Test, {
apply(target, thisArg, argumentsList) {
// 如果想要禁止使用非new的方式来调用函数,直接抛出异常即可
// throw new Error(`Function ${target.name} cannot be invoked without 'new'`)
return new (target.bind(thisArg, ...argumentsList))();
},
});
proxyClass(1, 2); // constructor 1 2
包装 fetch
js
let handlers = {
get(target, property) {
if (!target.init) {
// 初始化对象
['GET', 'POST'].forEach((method) => {
target[method] = (url, params = {}) => {
return fetch(url, {
headers: {
'content-type': 'application/json',
},
mode: 'cors',
credentials: 'same-origin',
method,
...params,
}).then((response) => response.json());
};
});
}
return target[property];
},
};
let API = new Proxy({}, handlers);
断言工具
js
let assert = new Proxy(
{},
{
set(target, message, value) {
if (!value) console.error(message);
},
},
);
assert["Isn't true"] = false; // Error: Isn't true
assert['Less than 18'] = 18 >= 19; // Error: Less than 18
统计函数调用次数
js
function orginFunction () {}
let proxyFunction = new Proxy(orginFunction, {
apply (target, thisArg. argumentsList) {
log(XXX)
return target.apply(thisArg, argumentsList)
}
})
实现双绑比 Object.defineProperty 的优势
- 可以直接监听对象而非属性
- 可以直接监听数组的变化
- 有多达 13 种拦截方法,不限于
apply、ownKeys、deleteProperty、has
等等