JavaScript的Proxy能够作哪些有意思的事儿

摘要: 神奇而有趣的Proxy。javascript

Fundebug经受权转载,版权归原做者全部。前端

Proxy是什么

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

能够理解为,有一个很火的明星,开通了一个微博帐号,这个帐号很是活跃,回复粉丝、处处点赞之类的,但可能并非真的由本人在维护的。json

而是在背后有一个其余人 or 团队来运营,咱们就能够称他们为代理人,由于他们发表的微博就表明了明星本人的意思。segmentfault

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

这个代入到JavaScript当中来,就能够理解为对对象或者函数的代理操做。cors

JavaScript中的Proxy

Proxy是ES6中提供的新的API,能够用来定义对象各类基本操做的自定义行为 (在文档中被称为traps,我以为能够理解为一个针对对象各类行为的钩子),拿它能够作不少有意思的事情,在咱们须要对一些对象的行为进行控制时将变得很是有效。函数

Proxy的语法

建立一个Proxy的实例须要传入两个参数工具

  1. target 要被代理的对象,能够是一个object或者function
  2. handlers对该代理对象的各类操做行为处理
let target = {}
let handlers = {} // do nothing
let proxy = new Proxy(target, handlers)

proxy.a = 123

console.log(target.a) // 123
复制代码

在第二个参数为空对象的状况下,基本能够理解为是对第一个参数作的一次浅拷贝 (Proxy必须是浅拷贝,若是是深拷贝则会失去了代理的意义)性能

Traps(各类行为的代理)

就像上边的示例代码同样,若是没有定义对应的trap,则不会起任何做用,至关于直接操做了target

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

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

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

let obj = {
  _age: 18,
  get age ()  {
    return `I'm ${this._age} years old`
  },
  set age (val) {
    this._age = Number(val)
  }
}

console.log(obj.age) // I'm 18 years old
obj.age = 19
console.log(obj.age) // I'm 19 years old
复制代码

就像这段代码描述的同样,咱们设置了一个属性_age,而后又设置了一个get ageset age

而后咱们能够直接调用obj.age来获取一个返回值,也能够对其进行赋值。

这么作有几个缺点:

  1. 针对每个要代理的属性都要编写对应的gettersetter
  2. 必须还要存在一个存储真实值的key(若是咱们直接在getter里边调用this.age则会出现堆栈溢出的状况,由于不管什么时候调用this.age进行取值都会触发getter)

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

let target = { age: 18, name: 'Niko Bellic' }
let handlers = {
  get (target, property) {
    return `${property}: ${target[property]}`
  },
  set (target, property, value) {
    target[property] = value
  }
}
let proxy = new Proxy(target, handlers)

proxy.age = 19
console.log(target.age, proxy.age)   // 19, age : 19
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能够很好的兼容这种状况。

(() => {
  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
})()
复制代码

咱们代理了get,并在里边进行逻辑处理,若是咱们要进行get的值来自一个不存在的key,则咱们会在target中建立对应个这个key,而后返回一个针对这个key的代理对象。

这样就可以保证咱们的取值操做必定不会抛出can not get xxx from undefined 可是这会有一个小缺点,就是若是你确实要判断这个key是否存在只可以经过in操做符来判断,而不可以直接经过get来判断。

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

若是咱们提供了一个Class对象给其余人,或者说一个ES5版本的构造函数。 若是没有使用new关键字来调用的话,Class对象会直接抛出异常,而ES5中的构造函数this指向则会变为调用函数时的做用域。 咱们可使用apply这个trap来兼容这种状况:

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
复制代码

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

以及若是咱们想要对函数进行限制,禁止使用new关键字来调用,能够用另外一个trap:construct

function add (a, b) {
  return a + b
}

let proxy = new Proxy(add, {
  construct (target, argumentsList, newTarget) {
    throw new Error(`Function ${target.name} cannot be invoked with 'new'`)
  }
})

proxy(1, 2)     // 3
new proxy(1, 2) // throw an error
复制代码

用Proxy来包装fetch

在前端发送请求,咱们如今常常用到的应该就是fetch了,一个原生提供的API。 咱们能够用Proxy来包装它,使其变得更易用。

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)

await API.GET('XXX')
await API.POST('XXX', {
  body: JSON.stringify({name: 1})
})
复制代码

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

实现一个简易的断言工具

写过测试的各位童鞋,应该都会知道断言这个东西 console.assert就是一个断言工具,接受两个参数,若是第一个为false,则会将第二个参数做为Error message抛出。 咱们可使用Proxy来作一个直接赋值就能实现断言的工具。

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
复制代码

统计函数调用次数

在作服务端时,咱们能够用Proxy代理一些函数,来统计一段时间内调用的次数。 在后期作性能分析时可能会可以用上:

function orginFunction () {}
let proxyFunction = new Proxy(orginFunction, {
  apply (target, thisArg. argumentsList) {
    log(XXX)

    return target.apply(thisArg, argumentsList)
  }
})


复制代码

所有的traps

这里列出了handlers全部能够定义的行为 (traps)

具体的能够查看MDN-Proxy 里边一样有一些例子

traps description
get 获取某个key
set 设置某个key
has 使用in操做符判断某个key是否存在
apply 函数调用,仅在代理对象为function时有效
ownKeys 获取目标对象全部的key
construct 函数经过实例化调用,仅在代理对象为function时有效
isExtensible 判断对象是否可扩展,Object.isExtensible的代理
deleteProperty 删除一个property
defineProperty 定义一个新的property
getPrototypeOf 获取原型对象
setPrototypeOf 设置原型对象
preventExtensions 设置对象为不可扩展
getOwnPropertyDescriptor 获取一个自有属性 (不会去原型链查找) 的属性描述

参考资料

相关文章
相关标签/搜索