ES6中的Reflect和Proxy对象允许开发人员访问以前隐藏在Javascript引擎内部的功能。我们广泛地在Reflect使用这些方法,为了拦截和重新定义核心的web APIs。

Reflect 和 Proxy

Reflect和Proxy在ES6规范中均是标准的内置对象,并且被所有的现代浏览器所支持。一般来说,它们在Javascript上下文里把元编程的定义正式化了,通过结合现有的内置APIs,并进一步扩展。在这篇文章中,我们使用了一些接近现实需求的例子,来探索它们是怎么使用。

前言

Javascript引擎中存在一些内置方法比如 [[GetOwnProperty]], [[HasProperty]], and [[Set]],这些方法在早前版本的标准中已经被暴露。如果您以前使用过Javascript,那么您可能对一些开发人员使用的替代方法很熟悉。例如:

const foo = { firstName: 'SomeFirstName', age: 99 }
Object.defineProperty(foo, 'lastName', { value: 'SomeLastName', enumerable: true })
const bar = Object.keys(foo) // ['firstName', 'age', 'lastName']
const baz = Object.values(foo) // ['SomeFirstName', 99, 'SomeLastName']
Object.hasOwnProperty.call(foo, 'lastName') // true

上面的示例演示了在全局对象上定义的静态内置方法。它们只代表了一小部分我们想要使用的常用的引擎内部方法,并且它们被附加到原型上。总之,Reflect和Proxy API统一并简化了这些现有方法,扩展了内部检查功能,并公开了以前不可能实现的交互API。

在本文中,我们将重点介绍我们在Reflect中最常用的函数,而不是讨论在这些对象上定义的每个函数。要了解更多关于Reflect的函数,我们建议阅读MDN指南。

Reflect 简单例子

假如有一个场景,你需要在每次访问全局对象里的字段时打印一些信息。你可能会在整个应用程序中在每次访问对象实例时手动地调用get()方法然后输出信息...

// app.ts
// On pageload, we fetch the global session
window.globalSession = fetchSession()

// file1.ts
// We've accessed a field on globalSession, and the developer has logged that
const firstName = globalSession.firstName
console.log('GOT FIELD firstName')

// file2.ts
// Same applies here
const lastName = globalSession.lastName
const age = globalSession.age
const firstRelative = globalSession.relatives[0]
console.log('GOT FIELD lastName')
console.log('GOT FIELD age')
console.log('GOT FIELD relatives[0]')

这个模式存在缺陷,有以下几个原因:

  1. 它需要专有知识:开发人员负责记住在每次访问globalSession上的某个字段时,还必须调用console.log()方法。这很难执行,也很容易忘记。
  2. 它不具扩展性:如果globalSession上的字段名称发生更改时,重构将是一场噩梦。如果您希望对globalSession以外的某些对象实现相同的策略,则需要重复整个原始过程,并进一步扩展在代码库中开发所需的专有知识。
  3. 它没有考虑到更复杂的场景:上面的示例演示了简单的访问模式,但是当你有如下情况时会发生什么?
// file3.ts
// Point another global to the global session
window.activeSession = globalSession

// file4.ts
// Don't forget that activeSession points to the same object as globalSession, you still need to call console.log()!
const middleName = activeSession.middleName

上述方法中的缺陷说明了我们试图表达的内容与我们如何实现解决方案之间的脱节。我们希望在每次访问某个对象上的字段时将一些信息记录到控制台。我们通过强制手动调用函数来解决这个问题。

Proxy对象恰好是我们需要的解决方法。以下是例子:

// makeStoreAccessProxy.ts
const makeStoreAccessProxy = (obj: Object) => {
  return new Proxy(obj, {
    get(target, key, receiver) {
      console.log(`GOT FIELD ${key}`)
      return Reflect.get(target, key)
    },
  })
}

// app.ts
window.globalSession = makeStoreAccessProxy(fetchSession())

每次(直接或间接)访问globalSession上的任何字段时,该访问将自动记录到控制台。

上述方法解决了缺陷:

  1. 不需要专有知识:开发人员可以访问globalSession上的字段,而无需记住存储有关所述访问的信息。
  2. 它可以扩展:重构globalSession与重构任何其他对象一样简单,并且相同的makeStoreAccessProxy函数可以随时用于整个代码库中的任何对象。
  3. 它适用更复杂的场景:如果通过指向globalSession的其他对象获取globalSession上的某个字段,则访问仍将记录到控制台。

请注意,我们利用了代理和反射API来实现所需的结果。我们将逐一回顾这一点:

const makeStoreAccessProxy = (obj: Object) => {
  // This function returns a proxy of the provided 'obj'. Without defining the second
  // 'handler' argument, this is a transparent passthrough to 'obj' and would behave as
  // though it _were_ the original 'obj'.
  return new Proxy(obj, {
    // We then define a 'get' function in the handler. This means that we're redefining
    // the fundamental get operation on 'obj'
    get(target, key, receiver) {
      // We've redefined 'get' to log information in the console
      console.log(`GOT FIELD ${key}`)
      // And finally, we're calling 'get' on the original unwrapped 'obj'. We could
      // instead return 'target[key]', but this demonstrates the consistency between
      // the Proxy and Reflect APIs
      return Reflect.get(target, key)
    }
  })
}

Proxy构造函数中的第二个参数handler的get()方法和Reflect对象的get()是保持一致的。你在Proxy handler中定义的每一个方法都有一个对应的方法在Reflect对象中。您可以创建一个完全没有意义的Proxy,它仅充当一个传递参数的角色来重写每一个支持的方法并简单的调用对应的Reflect方法。

const p = new Proxy({}, {
  defineProperty() { return Reflect.defineProperty(...arguments) },
  getPrototypeOf() { return Reflect.getPrototypeOf(...arguments) },
  get() { return Reflect.get(...arguments) },
  set() { return Reflect.set(...arguments) },
  ... // etc
})

Reflect 高级例子

在这个例子中,我们编写的代码是需要跟踪页面上所有的动态加载的图像。因为我们不能直接操作底层应用程序代码,所以我们需要某种机制来透明地捕获对src属性的访问…

// First we'll store a reference to the original property descriptor for the
// HTMLImageElement's src field
const originalImgSrc = Reflect.getOwnPropertyDescriptor(HTMLImageElement.prototype, 'src')

// Then we'll overwrite the HTMLImageElement prototype's "src" property and trap
// calls to that field's get() and set() methods
Reflect.defineProperty(HTMLImageElement.prototype, 'src', {
  get() {
    // When <someImg>.src is called anywhere, we'll log some information, then call the
    // target's get() method also using the Reflect API
    console.log('getting the src')
    return Reflect.apply(originalImgSrc.get, this, [])
  },
  set(value) {
    // When <someImg>.src = 'something' is called anywhere, we'll log some information, then call the
    // target's set() method also using the Reflect API
    console.log(`setting src to ${value}`)
    return Reflect.apply(originalImgSrc.set, this, [value])
  },
})

从应用程序的角度来看,这个变化是透明的。任何<img>节点的src属性都可以像不存在此覆盖一样进行操作。我们只是截取对这些字段的访问,采取一些行动,然后继续进行,就好像什么都没发生一样。底层应用程序不需要知道这种变化,并且在功能上保持不变。

Proxy 例子

我们是怎么使用Proxy对象?我们可能需要在某些库或框架的内部设置捕获行为,以便完全重新定义它们。让我们设想一个场景,其中一个框架有两个内部方法来操作DOM。两种方法都实现了相同的最终结果,但一种是异步的,而另一种不是。考虑到性能原因,异步的版本可能是更好的选择,但为了更精确地跟踪用户的操作,我们更推荐异步的方法,如果开发者仅使用了同步的方法。

有了Proxy,这不是一个问题,我们可以完全自己解决这个问题,而不需要应用程序更改自己的源代码。

const someFramework = document.querySelector('#framework-root').framework

someFramework.blockingUpdate = new Proxy(someFramework.blockingUpdate, {
  apply(target, thisArg, argArray) {
    // Here we'll take some action whenever a call to blockingUpdate() is made
    console.log('Intercepted a call to blockingUpdate()')
    Reflect.apply(target, thisArg, argArray)
  },
})

someFramework.asyncUpdate = new Proxy(someFramework.asyncUpdate, {
  apply(target, thisArg, argArray) {
    // Here we'll redefine calls to asyncUpdate() to instead invoke blockingUpdate()
    Reflect.apply(someFramework.blockingUpdate, thisArg, argArray)
  },
})

总结

在使用本文描述的API时,考虑周到是很重要的。一般来说,web应用程序不应该重新定义核心web API(我们认为Reflect的用例是一个例外),但是当代理和Reflect是正确的工作工具时,了解它们的工作方式也很重要。例如,在过去,我们使用Reflect.defineProperty函数来重新定义存在于web上许多站点上的全局第三方属性,但是当我们这样做时,我们忘记了包含enumerable:true字段。特别是一个站点依赖于可枚举的属性,因此当我们重新定义它时,他们站点上的一些功能在使用Reflect应用程序的上下文中停止工作。

Reflect(应用程序)可以被认为是一个自上而下的反射web应用程序容器,理想情况下,它对web应用程序的观察和操作是透明的。如果您想了解更多关于Reflect如何工作的信息,我们很乐意听到您的消息!你可以在info@reflect.run联系我们。 快乐地测试吧!


yannisLee
0 声望0 粉丝

偶尔翻译一些精品文章