查看原文
其他

拿 Proxy 可以做哪些有意思的事儿

前端大全 2022-07-13

(点击上方公众号,可快速关注)


作者:贾顺名

segmentfault.com/a/1190000015009255


Proxy是什么

首先,我们要清楚, Proxy是什么意思,这个单词翻译过来,就是 代理

可以理解为,有一个很火的明星,开通了一个微博账号,这个账号非常活跃,回复粉丝、到处点赞之类的,但可能并不是真的由本人在维护的。而是在背后有一个其他人 or 团队来运营,我们就可以称他们为代理人,因为他们发表的微博就代表了明星本人的意思。

P.S. 强行举例子,因为本人不追星,只是猜测可能会有这样的运营团队

这个代入到 JavaScript当中来,就可以理解为对 对象或者 函数的代理操作。

JavaScript中的Proxy

Proxy是ES6中提供的新的API,可以用来定义对象各种基本操作的自定义行为(在文档中被称为 traps,我觉得可以理解为一个针对对象各种行为的钩子)。拿它可以做很多有意思的事情,在我们需要对一些对象的行为进行控制时将变得非常有效。

Proxy的语法

创建一个 Proxy的实例需要传入两个参数

  1. target 要被代理的对象,可以是一个 object或者 function

  2. handlers对该代理对象的各种操作行为处理

  1. let target = {}

  2. let handlers = {} // do nothing

  3. let proxy = new Proxy(target, handlers)

  4. proxy.a = 123

  5. console.log(target.a) // 123

在第二个参数为空对象的情况下,基本可以理解为是对第一个参数做的一次浅拷贝(Proxy必须是浅拷贝,如果是深拷贝则会失去了代理的意义)。

Traps(各种行为的代理)

就像上边的示例代码一样,如果没有定义对应的 trap,则不会起任何作用,相当于直接操作了 target

当我们写了某个 trap以后,在做对应的动作时,就会触发我们的回调函数,由我们来控制被代理对象的行为。

最常用的两个 trap应该就是 getset了。

早年 JavaScript有着在定义对象时针对某个属性进行设置 gettersetter

  1. let obj = {

  2.  _age: 18,

  3.  get age ()  {

  4.    return `I'm ${this._age} years old`

  5.  },

  6.  set age (val) {

  7.    this._age = Number(val)

  8.  }

  9. }

  10. console.log(obj.age) // I'm 18 years old

  11. obj.age = 19

  12. console.log(obj.age) // I'm 19 years old

就像这段代码描述的一样,我们设置了一个属性 _age,然后又设置了一个 getagesetage。然后我们可以直接调用 obj.age来获取一个返回值,也可以对其进行赋值。

这么做有几个缺点:

  1. 针对每一个要代理的属性都要编写对应的 getter、 setter

  2. 必须还要存在一个存储真实值的 key(如果我们直接在 getter里边调用 this.age则会出现堆栈溢出的情况,因为无论何时调用 this.age进行取值都会触发 getter)。

Proxy很好的解决了这两个问题:

  1. let target = { age: 18, name: 'Niko Bellic' }

  2. let handlers = {

  3.  get (target, property) {

  4.    return `${property}: ${target[property]}`

  5.  },

  6.  set (target, property, value) {

  7.    target[property] = value

  8.  }

  9. }

  10. let proxy = new Proxy(target, handlers)

  11. proxy.age = 19

  12. console.log(target.age, proxy.age)   // 19,          age : 19

  13. console.log(target.name, proxy.name) // Niko Bellic, name: Niko Bellic

我们通过创建 getset两个 trap来统一管理所有的操作,可以看到,在修改 proxy的同时, target的内容也被修改,而且我们对 proxy的行为进行了一些特殊的处理。而且我们无需额外的用一个 key来存储真实的值,因为我们在 trap内部操作的是 target对象,而不是 proxy对象。

拿Proxy来做些什么

因为在使用了 Proxy后,对象的行为基本上都是可控的,所以我们能拿来做一些之前实现起来比较复杂的事情。

在下边列出了几个简单的适用场景。

解决对象属性为undefined的问题

在一些层级比较深的对象属性获取中,如何处理 undefined一直是一个痛苦的过程,如果我们用 Proxy可以很好的兼容这种情况。

  1. (() => {

  2.  let target = {}

  3.  let handlers = {

  4.    get: (target, property) => {

  5.      target[property] = (property in target) ? target[property] : {}

  6.      if (typeof target[property] === 'object') {

  7.        return new Proxy(target[property], handlers)

  8.      }

  9.      return target[property]

  10.    }

  11.  }

  12.  let proxy = new Proxy(target, handlers)

  13.  console.log('z' in proxy.x.y) // false (其实这一步已经针对`target`创建了一个x.y的属性)

  14.  proxy.x.y.z = 'hello'

  15.  console.log('z' in proxy.x.y) // true

  16.  console.log(target.x.y.z)     // hello

  17. })()

我们代理了 get,并在里边进行逻辑处理,如果我们要进行 get的值来自一个不存在的 key,则我们会在 target中创建对应个这个 key,然后返回一个针对这个 key的代理对象。这样就能够保证我们的取值操作一定不会抛出 cannotgetxxxfromundefined

但是这会有一个小缺点,就是如果你确实要判断这个 key是否存在只能够通过 in操作符来判断,而不能够直接通过 get来判断。

普通函数与构造函数的兼容处理

如果我们提供了一个 Class对象给其他人,或者说一个 ES5版本的构造函数。

如果没有使用 new关键字来调用的话, Class对象会直接抛出异常,而 ES5中的构造函数 this指向则会变为调用函数时的作用域。

我们可以使用 apply这个 trap来兼容这种情况:

  1. class Test {

  2.  constructor (a, b) {

  3.    console.log('constructor', a, b)

  4.  }

  5. }

  6. // Test(1, 2) // throw an error

  7. let proxyClass = new Proxy(Test, {

  8.  apply (target, thisArg, argumentsList) {

  9.    // 如果想要禁止使用非new的方式来调用函数,直接抛出异常即可

  10.    // throw new Error(`Function ${target.name} cannot be invoked without 'new'`)

  11.    return new (target.bind(thisArg, ...argumentsList))()

  12.  }

  13. })

  14. proxyClass(1, 2) // constructor 1 2

我们使用了 apply来代理一些行为,在函数调用时会被触发,因为我们明确的知道,代理的是一个 Class或构造函数,所以我们直接在 apply中使用 new关键字来调用被代理的函数。

以及如果我们想要对函数进行限制,禁止使用 new关键字来调用,可以用另一个 trap: construct

  1. function add (a, b) {

  2.  return a + b

  3. }

  4. let proxy = new Proxy(add, {

  5.  construct (target, argumentsList, newTarget) {

  6.    throw new Error(`Function ${target.name} cannot be invoked with 'new'`)

  7.  }

  8. })

  9. proxy(1, 2)     // 3

  10. new proxy(1, 2) // throw an error

用Proxy来包装fetch

在前端发送请求,我们现在经常用到的应该就是 fetch了,一个原生提供的API。

我们可以用 Proxy来包装它,使其变得更易用。

  1. let handlers = {

  2.  get (target, property) {

  3.    if (!target.init) {

  4.      // 初始化对象

  5.      ['GET', 'POST'].forEach(method => {

  6.        target[method] = (url, params = {}) => {

  7.          return fetch(url, {

  8.            headers: {

  9.              'content-type': 'application/json'

  10.            },

  11.            mode: 'cors',

  12.            credentials: 'same-origin',

  13.            method,

  14.            ...params

  15.          }).then(response => response.json())

  16.        }

  17.      })

  18.    }

  19.    return target[property]

  20.  }

  21. }

  22. let API = new Proxy({}, handlers)

  23. await API.GET('XXX')

  24. await API.POST('XXX', {

  25.  body: JSON.stringify({name: 1})

  26. })

GETPOST进行了一层封装,可以直接通过 .GET这种方式来调用,并设置一些通用的参数。

实现一个简易的断言工具

写过测试的各位童鞋,应该都会知道断言这个东西。 console.assert就是一个断言工具,接受两个参数,如果第一个为 false,则会将第二个参数作为 Errormessage抛出。

我们可以使用 Proxy来做一个直接赋值就能实现断言的工具。

  1. let assert = new Proxy({}, {

  2.  set (target, message, value) {

  3.    if (!value) console.error(message)

  4.  }

  5. })

  6. assert['Isn\'t true'] = false      // Error: Isn't true

  7. assert['Less than 18'] = 18 >= 19  // Error: Less than 18

统计函数调用次数

在做服务端时,我们可以用 Proxy代理一些函数,来统计一段时间内调用的次数。

在后期做性能分析时可能会能够用上:

  1. function orginFunction () {}

  2. let proxyFunction = new Proxy(orginFunction, {

  3.  apply (target, thisArg. argumentsList) {

  4.    log(XXX)

  5.    return target.apply(thisArg, argumentsList)

  6.  }

  7. })

全部的traps

这里列出了 handlers所有可以定义的行为 (traps):

具体的可以查看MDN-Proxy里边同样有一些例子

参考资料

  1. Magic Methods in JavaScript? Meet Proxy!

  2. How to use JavaScript Proxies for Fun and Profit

  3. MDN-Proxy



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

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

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存