Photo by Fabian Grohs on Unsplashjavascript
Proxy 对象(Proxy)是 ES6 的一个很是酷却不为人知的特性。虽然这个特性存在已久,可是我仍是想在本文中对其稍做解释,并用一个例子说明一下它的用法。前端
正如 MDN 上简单而枯燥的定义:java
Proxy 对象用于定义基本操做的自定义行为(如属性查找,赋值,枚举,函数调用等)。
虽然这是一个不错的总结,可是我却并无从中搞清楚 Proxy 能作什么,以及它能帮咱们实现什么。android
首先,Proxy 的概念来源于元编程。简单的说,元编程是容许咱们运行咱们编写的应用程序(或核心)代码的代码。例如,臭名昭著的 eval
函数容许咱们将字符串代码当作可执行代码来执行,它是就属于元编程领域。ios
Proxy
API 容许咱们在对象和其消费实体中建立中间层,这种特性为咱们提供了控制该对象的能力,好比能够决定怎样去进行它的 get
和 set
,甚至能够自定义当访问这个对象上不存在的属性的时候咱们能够作些什么。git
var p = new Proxy(target, handler);
Proxy
构造函数获取一个 target
对象,和一个用来拦截 target
对象不一样行为的 handler
对象。你能够设置下面这些拦截项:github
has
— 拦截 in
操做。好比,你能够用它来隐藏对象上某些属性。get
— 用来拦截读取操做。好比当试图读取不存在的属性时,你能够用它来返回默认值。set
— 用来拦截赋值操做。好比给属性赋值的时候你能够增长验证的逻辑,若是验证不经过能够抛出错误。apply
— 用来拦截函数调用操做。好比,你能够把全部的函数调用都包裹在 try/catch
语句块中。这只是一部分拦截项,你能够在 MDN 上找到完整的列表。编程
下面是将 Proxy 用在验证上的一个简单的例子:后端
const Car = { maker: 'BMW', year: 2018, }; const proxyCar = new Proxy(Car, { set(obj, prop, value) { if (prop === 'maker' && value.length < 1) { throw new Error('Invalid maker'); } if (prop === 'year' && typeof value !== 'number') { throw new Error('Invalid year'); } obj[prop] = value; return true; } }); proxyCar.maker = ''; // throw exception proxyCar.year = '1999'; // throw exception
能够看到,咱们能够用 Proxy 来验证赋给被代理对象的值。bash
为了在实践中展现 Proxy 的能力,我建立了一个简单的监测库,用来监测给定的对象或类,监测项以下:
这是经过在访问任意对象、类、甚至是函数时,调用一个名为 proxyTrack
的函数来完成的。
若是你但愿监测是谁给一个对象的属性赋的值,或者一个函数执行了多久、执行了多少次、谁执行的,这个库将很是有用。我知道可能还有其余更好的工具来实现上面的功能,可是在这里我建立这个库就是为了用一用这个 API。
首先,咱们看看怎么用:
function MyClass() {} MyClass.prototype = { isPrime: function() { const num = this.num; for(var i = 2; i < num; i++) if(num % i === 0) return false; 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 time MyClass.num is being get by isPrime for the 1 time MyClass.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
来实时监控它。好比:
class MyComponent extends Component{...} export default connect(mapStateToProps)(proxyTrack(MyComponent, { trackFunctions: true, trackProps: true, trackTime: true, trackCaller: true, trackCount: true, filter: (data) => { if( data.type === 'get' && data.prop === 'componentDidUpdate') return false; return true; } }));
能够看到,你能够将你不关心的信息过滤掉,不然输出将会变得杂乱无章。
咱们来看看 proxyTrack
的实现。
首先是这个函数自己:
export function 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 = new Proxy(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] = new Proxy(trackedEntity[name], { apply: trackFunctionCall(options), }); } }); }
能够看到,假如咱们但愿监测对象的属性,咱们建立了一个带有 get
和 set
拦截器的被监测对象。下面是 set
拦截器的实现:
function trackPropertySet(options = {}) { return function set(target, prop, value, receiver) { const { trackCaller, trackCount, stdout, filter } = options; const error = trackCaller && new Error(); 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); } } return Reflect.set(target, prop, value, receiver); }; }
更有趣的是 trackClass
函数(至少对我来讲是这样):
function trackClass(cls, options = {}) { cls.prototype = trackObject(cls.prototype, options); cls.prototype.constructor = cls; return new Proxy(cls, { construct(target, args) { const obj = new target(...args); return new Proxy(obj, { get: trackPropertyGet(options), set: trackPropertySet(options), }); }, apply: trackFunctionCall(options), }); }
在这个案例中,由于咱们但愿拦截这个类上不属于原型上的属性,因此咱们给这个类的原型建立了个代理,而且建立了个构造函数拦截器。
别忘了,即便你在原型上定义了一个属性,但若是你再给这个对象赋值一个同名属性,JavaScript 将会建立一个这个属性的本地副本,因此赋值的改动并不会改变这个类其余实例的行为。这就是为什么只对原型作代理并不能知足要求的缘由。
戳这里查看完整代码。
若是发现译文存在错误或其余须要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可得到相应奖励积分。文章开头的 本文永久连接 即为本文在 GitHub 上的 MarkDown 连接。
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、 iOS、 前端、 后端、 区块链、 产品、 设计、 人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、 官方微博、 知乎专栏。