网站首页 > 技术文章 正文
Proxy 对象(Proxy)是 ES6 的一个非常酷却鲜为人知的特性。虽然这个特性存在已久,但是我还是想在本文中对其稍作解释,并用一个例子说明一下它的用法。
什么是 Proxy
正如 MDN 上简单而枯燥的定义:
Proxy 对象用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等)。
虽然这是一个不错的总结,但是我却并没有从中搞清楚 Proxy 能做什么,以及它能帮我们实现什么。
首先,Proxy 的概念来源于元编程。简单的说,元编程是允许我们运行我们编写的应用程序(或核心)代码的代码。例如,臭名昭著的 eval 函数允许我们将字符串代码当做可执行代码来执行,它是就属于元编程领域。
Proxy API 允许我们在对象和其消费实体中创建中间层,这种特性为我们提供了控制该对象的能力,比如可以决定怎样去进行它的 get 和 set,甚至可以自定义当访问这个对象上不存在的属性的时候我们可以做些什么。
Proxy 的 API
var p = newProxy(target, handler);
Proxy 构造函数获取一个 target 对象,和一个用来拦截 target 对象不同行为的 handler 对象。你可以设置下面这些拦截项:
- has —?拦截 in 操作。比如,你可以用它来隐藏对象上某些属性。
- get —?用来拦截读取操作。比如当试图读取不存在的属性时,你可以用它来返回默认值。
- set — 用来拦截赋值操作。比如给属性赋值的时候你可以增加验证的逻辑,如果验证不通过可以抛出错误。
- apply — 用来拦截函数调用操作。比如,你可以把所有的函数调用都包裹在 try/catch 语句块中。
这只是一部分拦截项,你可以在 MDN 上找到完整的列表。
下面是将 Proxy 用在验证上的一个简单的例子:
constCar= { maker: 'BMW', year: 2018,}; const proxyCar = newProxy(Car, {set(obj, prop, value) {if(prop === 'maker'&& value.length < 1) {thrownewError('Invalid maker');} if(prop === 'year'&& typeof value !== 'number') {thrownewError('Invalid year');} obj[prop] = value;returntrue;} }); proxyCar.maker = ''; // throw exceptionproxyCar.year = '1999'; // throw exception
可以看到,我们可以用 Proxy 来验证赋给被代理对象的值。
使用 Proxy 来调试
为了在实践中展示 Proxy 的能力,我创建了一个简单的监测库,用来监测给定的对象或类,监测项如下:
- 函数执行时间
- 函数的调用者或属性的访问者
- 统计每个函数或属性的被访问次数。
这是通过在访问任意对象、类、甚至是函数时,调用一个名为 proxyTrack 的函数来完成的。
如果你希望监测是谁给一个对象的属性赋的值,或者一个函数执行了多久、执行了多少次、谁执行的,这个库将非常有用。我知道可能还有其他更好的工具来实现上面的功能,但是在这里我创建这个库就是为了用一用这个 API。
使用 proxyTrack
首先,我们看看怎么用:
functionMyClass() {} MyClass.prototype = { isPrime: function() {const num = this.num;for(var i = 2; i < num; i++)if(num % i === 0) returnfalse;return num !== 1&& num !== 0;}, num: null,}; MyClass.prototype.constructor = MyClass; const trackedClass = proxyTrack(MyClass); function start() {const my = new trackedClass(); my.num = 573723653;if(!my.isPrime()) {return`${my.num} is not prime`;}} function main() { start();} main();
如果我们运行这段代码,控制台将会输出:
MyClass.num is being set by start for the 1 timeMyClass.num is being get by isPrime for the 1 timeMyClass.isPrime was called by start for the 1 time and took 0 mils.MyClass.num is being get by start for the 2 time
proxyTrack 接受 2 个参数:第一个是要监测的对象/类,第二个是一个配置项对象,如果没传递的话将被置为默认值。我们看看这个配置项默认值长啥样:
const defaultOptions = { trackFunctions: true, trackProps: true, trackTime: true, trackCaller: true, trackCount: true, stdout: null, filter: null,};
可以看到,你可以通过配置你关心的监测项来监测你的目标。比如你希望将结果输出出来,那么你可以将 console.log 赋给 stdout。
还可以通过赋给 filter 的回调函数来自定义地控制输出哪些信息。你将会得到一个包括有监测信息的对象,并且如果你希望保留这个信息就返回 true,反之返回 false。
在 React 中使用 proxyTrack
因为 React 的组件实际上也是类,所以你可以通过 proxyTrack 来实时监控它。比如:
classMyComponent extends Component{...} exportdefault connect(mapStateToProps)(proxyTrack(MyComponent, { trackFunctions: true, trackProps: true, trackTime: true, trackCaller: true, trackCount: true, filter: (data) => {if( data.type === 'get'&& data.prop === 'componentDidUpdate') returnfalse;returntrue;}}));
可以看到,你可以将你不关心的信息过滤掉,否则输出将会变得杂乱无章。
实现 proxyTrack
我们来看看 proxyTrack 的实现。
首先是这个函数本身:
exportfunction proxyTrack(entity, options = defaultOptions) {if(typeof entity === 'function') return trackClass(entity, options);return trackObject(entity, options);}
没什么特别的嘛,这里只是调用相关函数。
再看看 trackObject:
function trackObject(obj, options = {}) {const{ trackFunctions, trackProps } = options; let resultObj = obj;if(trackFunctions) { proxyFunctions(resultObj, options);}if(trackProps) { resultObj = newProxy(resultObj, {get: trackPropertyGet(options),set: trackPropertySet(options),});}return resultObj;}function proxyFunctions(trackedEntity, options) {if(typeof trackedEntity === 'function') return;Object.getOwnPropertyNames(trackedEntity).forEach((name) => {if(typeof trackedEntity[name] === 'function') { trackedEntity[name] = newProxy(trackedEntity[name], { apply: trackFunctionCall(options),});}});}
可以看到,假如我们希望监测对象的属性,我们创建了一个带有 get 和 set 拦截器的被监测对象。下面是 set 拦截器的实现:
function trackPropertySet(options = {}) {returnfunctionset(target, prop, value, receiver) {const{ trackCaller, trackCount, stdout, filter } = options;const error = trackCaller && newError();const caller = getCaller(error);const contextName = target.constructor.name === 'Object'? '': `${target.constructor.name}.`;const name = `${contextName}${prop}`;const hashKey = `set_${name}`;if(trackCount) {if(!callerMap[hashKey]) { callerMap[hashKey] = 1;} else{ callerMap[hashKey]++;}} let output = `${name} is being set`;if(trackCaller) { output += ` by ${caller.name}`;}if(trackCount) { output += ` for the ${callerMap[hashKey]} time`;} let canReport = true;if(filter) { canReport = filter({ type: 'get', prop, name, caller, count: callerMap[hashKey], value,});}if(canReport) {if(stdout) { stdout(output);} else{ console.log(output);}}returnReflect.set(target, prop, value, receiver);};}
更有趣的是 trackClass 函数(至少对我来说是这样):
function trackClass(cls, options = {}) { cls.prototype = trackObject(cls.prototype, options); cls.prototype.constructor = cls; returnnewProxy(cls, { construct(target, args) {const obj = new target(...args);returnnewProxy(obj, {get: trackPropertyGet(options),set: trackPropertySet(options),});}, apply: trackFunctionCall(options),});}
在这个案例中,因为我们希望拦截这个类上不属于原型上的属性,所以我们给这个类的原型创建了个代理,并且创建了个构造函数拦截器。
别忘了,即使你在原型上定义了一个属性,但如果你再给这个对象赋值一个同名属性,JavaScript 将会创建一个这个属性的本地副本,所以赋值的改动并不会改变这个类其他实例的行为。这就是为何只对原型做代理并不能满足要求的原因。
- 上一篇: 前端笔记-js浅拷贝和深拷贝
- 下一篇: 「JavaScript 从入门到精通」19.相等性判断
猜你喜欢
- 2024-11-23 被忽略的Set
- 2024-11-23 8个常用的JavaScript数组方法
- 2024-11-23 「JavaScript 从入门到精通」19.相等性判断
- 2024-11-23 前端笔记-js浅拷贝和深拷贝
- 2024-11-23 DOM 高级工程师不完全指南
- 2024-11-23 软件定义存储之ScaleIO、命令行操作和常用API使用指南
- 2024-11-23 Javascript的JSON.stringify()知多少?
- 2024-11-23 推荐10个“不能错过”的前端攻城师必背面试题源码及详解
- 2024-11-23 Web 前端怎样入门?
- 2024-11-23 JavaScript优化技巧
- 标签列表
-
- content-disposition (47)
- nth-child (56)
- math.pow (44)
- 原型和原型链 (63)
- canvas mdn (36)
- css @media (49)
- promise mdn (39)
- readasdataurl (52)
- if-modified-since (49)
- css ::after (50)
- border-image-slice (40)
- flex mdn (37)
- .join (41)
- function.apply (60)
- input type number (64)
- weakmap (62)
- js arguments (45)
- js delete方法 (61)
- blob type (44)
- math.max.apply (51)
- js (44)
- firefox 3 (47)
- cssbox-sizing (52)
- js删除 (49)
- js for continue (56)
- 最新留言
-