如何使用 Proxy 来代理 JavaScript 里的类

2019 年 10 月 20 日 前端大全

(给前端大全加星标,提升前端技能

作者:前端下午茶  公号 / SHERlocked93

Proxy 对象(Proxy)是 ES6 的一个非常酷却鲜为人知的特性。虽然这个特性存在已久,但是我还是想在本文中对其稍作解释,并用一个例子说明一下它的用法。

什么是 Proxy

正如 MDN 上简单而枯燥的定义:

Proxy 对象用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等)。

虽然这是一个不错的总结,但是我却并没有从中搞清楚 Proxy 能做什么,以及它能帮我们实现什么。

首先,Proxy 的概念来源于元编程。简单的说,元编程是允许我们运行我们编写的应用程序(或核心)代码的代码。例如,臭名昭著的 eval 函数允许我们将字符串代码当做可执行代码来执行,它是就属于元编程领域。

Proxy API 允许我们在对象和其消费实体中创建中间层,这种特性为我们提供了控制该对象的能力,比如可以决定怎样去进行它的 getset,甚至可以自定义当访问这个对象上不存在的属性的时候我们可以做些什么。

Proxy 的 API

  
  
    
  1. var p = new Proxy(target, handler);

Proxy 构造函数获取一个 target 对象,和一个用来拦截 target 对象不同行为的 handler 对象。你可以设置下面这些拦截项:

  • has — 拦截 in 操作。比如,你可以用它来隐藏对象上某些属性。

  • get — 用来拦截读取操作。比如当试图读取不存在的属性时,你可以用它来返回默认值。

  • set — 用来拦截赋值操作。比如给属性赋值的时候你可以增加验证的逻辑,如果验证不通过可以抛出错误。

  • apply — 用来拦截函数调用操作。比如,你可以把所有的函数调用都包裹在 try/catch 语句块中。

这只是一部分拦截项,你可以在 MDN 上找到完整的列表。

下面是将 Proxy 用在验证上的一个简单的例子:

  
  
    
  1. const Car = {

  2. maker: 'BMW',

  3. year: 2018,

  4. };


  5. const proxyCar = new Proxy(Car, {

  6. set(obj, prop, value) {

  7. if (prop === 'maker' && value.length < 1) {

  8. throw new Error('Invalid maker');

  9. }


  10. if (prop === 'year' && typeof value !== 'number') {

  11. throw new Error('Invalid year');

  12. }

  13. obj[prop] = value;

  14. return true;

  15. }


  16. });


  17. proxyCar.maker = ''; // throw exception

  18. proxyCar.year = '1999'; // throw exception

可以看到,我们可以用 Proxy 来验证赋给被代理对象的值。

使用 Proxy 来调试

为了在实践中展示 Proxy 的能力,我创建了一个简单的监测库,用来监测给定的对象或类,监测项如下:

  • 函数执行时间

  • 函数的调用者或属性的访问者

  • 统计每个函数或属性的被访问次数。

这是通过在访问任意对象、类、甚至是函数时,调用一个名为 proxyTrack 的函数来完成的。

如果你希望监测是谁给一个对象的属性赋的值,或者一个函数执行了多久、执行了多少次、谁执行的,这个库将非常有用。我知道可能还有其他更好的工具来实现上面的功能,但是在这里我创建这个库就是为了用一用这个 API。

使用 proxyTrack

首先,我们看看怎么用:

  
  
    
  1. function MyClass() {}


  2. MyClass.prototype = {

  3. isPrime: function() {

  4. const num = this.num;

  5. for(var i = 2; i < num; i++)

  6. if(num % i === 0) return false;

  7. return num !== 1 && num !== 0;

  8. },


  9. num: null,

  10. };


  11. MyClass.prototype.constructor = MyClass;


  12. const trackedClass = proxyTrack(MyClass);


  13. function start() {

  14. const my = new trackedClass();

  15. my.num = 573723653;

  16. if (!my.isPrime()) {

  17. return `${my.num} is not prime`;

  18. }

  19. }


  20. function main() {

  21. start();

  22. }


  23. main();

如果我们运行这段代码,控制台将会输出:

  
  
    
  1. MyClass.num is being set by start for the 1 time

  2. MyClass.num is being get by isPrime for the 1 time

  3. MyClass.isPrime was called by start for the 1 time and took 0 mils.

  4. MyClass.num is being get by start for the 2 time

proxyTrack 接受 2 个参数:第一个是要监测的对象/类,第二个是一个配置项对象,如果没传递的话将被置为默认值。我们看看这个配置项默认值长啥样:

  
  
    
  1. const defaultOptions = {

  2. trackFunctions: true,

  3. trackProps: true,

  4. trackTime: true,

  5. trackCaller: true,

  6. trackCount: true,

  7. stdout: null,

  8. filter: null,

  9. };

可以看到,你可以通过配置你关心的监测项来监测你的目标。比如你希望将结果输出出来,那么你可以将 console.log 赋给 stdout

还可以通过赋给 filter 的回调函数来自定义地控制输出哪些信息。你将会得到一个包括有监测信息的对象,并且如果你希望保留这个信息就返回 true,反之返回 false

在 React 中使用 proxyTrack

因为 React 的组件实际上也是类,所以你可以通过 proxyTrack 来实时监控它。比如:

  
  
    
  1. class MyComponent extends Component{...}


  2. export default connect(mapStateToProps)(proxyTrack(MyComponent, {

  3. trackFunctions: true,

  4. trackProps: true,

  5. trackTime: true,

  6. trackCaller: true,

  7. trackCount: true,

  8. filter: (data) => {

  9. if( data.type === 'get' && data.prop === 'componentDidUpdate') return false;

  10. return true;

  11. }

  12. }));

可以看到,你可以将你不关心的信息过滤掉,否则输出将会变得杂乱无章。

实现 proxyTrack

我们来看看 proxyTrack 的实现。

首先是这个函数本身:

  
  
    
  1. export function proxyTrack(entity, options = defaultOptions) {

  2. if (typeof entity === 'function') return trackClass(entity, options);

  3. return trackObject(entity, options);

  4. }

没什么特别的嘛,这里只是调用相关函数。

再看看 trackObject

  
  
    
  1. function trackObject(obj, options = {}) {

  2. const { trackFunctions, trackProps } = options;


  3. let resultObj = obj;

  4. if (trackFunctions) {

  5. proxyFunctions(resultObj, options);

  6. }

  7. if (trackProps) {

  8. resultObj = new Proxy(resultObj, {

  9. get: trackPropertyGet(options),

  10. set: trackPropertySet(options),

  11. });

  12. }

  13. return resultObj;

  14. }

  15. function proxyFunctions(trackedEntity, options) {

  16. if (typeof trackedEntity === 'function') return;

  17. Object.getOwnPropertyNames(trackedEntity).forEach((name) => {

  18. if (typeof trackedEntity[name] === 'function') {

  19. trackedEntity[name] = new Proxy(trackedEntity[name], {

  20. apply: trackFunctionCall(options),

  21. });

  22. }

  23. });

  24. }

可以看到,假如我们希望监测对象的属性,我们创建了一个带有 getset 拦截器的被监测对象。下面是 set 拦截器的实现:

  
  
    
  1. function trackPropertySet(options = {}) {

  2. return function set(target, prop, value, receiver) {

  3. const { trackCaller, trackCount, stdout, filter } = options;

  4. const error = trackCaller && new Error();

  5. const caller = getCaller(error);

  6. const contextName = target.constructor.name === 'Object' ? '' : `${target.constructor.name}.`;

  7. const name = `${contextName}${prop}`;

  8. const hashKey = `set_${name}`;

  9. if (trackCount) {

  10. if (!callerMap[hashKey]) {

  11. callerMap[hashKey] = 1;

  12. } else {

  13. callerMap[hashKey]++;

  14. }

  15. }

  16. let output = `${name} is being set`;

  17. if (trackCaller) {

  18. output += ` by ${caller.name}`;

  19. }

  20. if (trackCount) {

  21. output += ` for the ${callerMap[hashKey]} time`;

  22. }

  23. let canReport = true;

  24. if (filter) {

  25. canReport = filter({

  26. type: 'get',

  27. prop,

  28. name,

  29. caller,

  30. count: callerMap[hashKey],

  31. value,

  32. });

  33. }

  34. if (canReport) {

  35. if (stdout) {

  36. stdout(output);

  37. } else {

  38. console.log(output);

  39. }

  40. }

  41. return Reflect.set(target, prop, value, receiver);

  42. };

  43. }

更有趣的是 trackClass 函数(至少对我来说是这样):

  
  
    
  1. function trackClass(cls, options = {}) {

  2. cls.prototype = trackObject(cls.prototype, options);

  3. cls.prototype.constructor = cls;


  4. return new Proxy(cls, {

  5. construct(target, args) {

  6. const obj = new target(...args);

  7. return new Proxy(obj, {

  8. get: trackPropertyGet(options),

  9. set: trackPropertySet(options),

  10. });

  11. },

  12. apply: trackFunctionCall(options),

  13. });

  14. }

在这个案例中,因为我们希望拦截这个类上不属于原型上的属性,所以我们给这个类的原型创建了个代理,并且创建了个构造函数拦截器。

别忘了,即使你在原型上定义了一个属性,但如果你再给这个对象赋值一个同名属性,JavaScript 将会创建一个这个属性的本地副本,所以赋值的改动并不会改变这个类其他实例的行为。这就是为何只对原型做代理并不能满足要求的原因。



推荐阅读

(点击标题可跳转阅读)

如何用120行代码,实现一个交互完整的拖拽上传组件?

JavaScript 工具函数大全

深入了解 ES6 强大的  ...  运算符


觉得本文对你有帮助?请分享给更多人

关注「前端大全」加星标,提升前端技能

好文章,我在看❤️

登录查看更多
1

相关内容

元编程又译超编程,是指某类计算机程序的编写,这类计算机程序编写或者操纵其它程序(或者自身)作为它们的资料,或者在运行时完成部分本应在编译时完成的工作。多数情况下,与手工编写全部代码相比,程序员可以获得更高的工作效率,或者给与程序更大的灵活度去处理新的情形而无需重新编译。
【2020新书】使用高级C# 提升你的编程技能,412页pdf
专知会员服务
57+阅读 · 2020年6月26日
【2020新书】C++20 特性 第二版,A Problem-Solution Approach
专知会员服务
58+阅读 · 2020年4月26日
【干货书】流畅Python,766页pdf,中英文版
专知会员服务
224+阅读 · 2020年3月22日
【新书】Java企业微服务,Enterprise Java Microservices,272页pdf
TensorFlow Lite指南实战《TensorFlow Lite A primer》,附48页PPT
专知会员服务
69+阅读 · 2020年1月17日
【电子书】C++ Primer Plus 第6版,附PDF
专知会员服务
87+阅读 · 2019年11月25日
注意力机制介绍,Attention Mechanism
专知会员服务
168+阅读 · 2019年10月13日
一个牛逼的 Python 调试工具
机器学习算法与Python学习
15+阅读 · 2019年4月30日
如何编写完美的 Python 命令行程序?
CSDN
5+阅读 · 2019年1月19日
React Native 分包哪家强?看这文就够了!
程序人生
13+阅读 · 2019年1月16日
WebAssembly在QQ邮箱中的一次实践
IMWeb前端社区
13+阅读 · 2018年12月19日
.NET Core 环境下构建强大且易用的规则引擎
浅谈浏览器 http 的缓存机制
前端大全
6+阅读 · 2018年1月21日
Caffe 深度学习框架上手教程
黑龙江大学自然语言处理实验室
14+阅读 · 2016年6月12日
Seeing What a GAN Cannot Generate
Arxiv
8+阅读 · 2019年10月24日
Arxiv
6+阅读 · 2018年4月4日
Arxiv
4+阅读 · 2017年7月25日
VIP会员
相关VIP内容
【2020新书】使用高级C# 提升你的编程技能,412页pdf
专知会员服务
57+阅读 · 2020年6月26日
【2020新书】C++20 特性 第二版,A Problem-Solution Approach
专知会员服务
58+阅读 · 2020年4月26日
【干货书】流畅Python,766页pdf,中英文版
专知会员服务
224+阅读 · 2020年3月22日
【新书】Java企业微服务,Enterprise Java Microservices,272页pdf
TensorFlow Lite指南实战《TensorFlow Lite A primer》,附48页PPT
专知会员服务
69+阅读 · 2020年1月17日
【电子书】C++ Primer Plus 第6版,附PDF
专知会员服务
87+阅读 · 2019年11月25日
注意力机制介绍,Attention Mechanism
专知会员服务
168+阅读 · 2019年10月13日
相关资讯
一个牛逼的 Python 调试工具
机器学习算法与Python学习
15+阅读 · 2019年4月30日
如何编写完美的 Python 命令行程序?
CSDN
5+阅读 · 2019年1月19日
React Native 分包哪家强?看这文就够了!
程序人生
13+阅读 · 2019年1月16日
WebAssembly在QQ邮箱中的一次实践
IMWeb前端社区
13+阅读 · 2018年12月19日
.NET Core 环境下构建强大且易用的规则引擎
浅谈浏览器 http 的缓存机制
前端大全
6+阅读 · 2018年1月21日
Caffe 深度学习框架上手教程
黑龙江大学自然语言处理实验室
14+阅读 · 2016年6月12日
Top
微信扫码咨询专知VIP会员