ES6: Proxy, Reflect and Object.defineProperty

2017年4月9日 15:14:00 PM
ES6

Reflection 初遇,是在 Java 中,年代已经有些久远。

犹记那时,风轻云淡,佳酿红颜,恰同学少年。

现今打开 Oracle 官网,上面是这样描述 Reflection

It allows an executing Java program to examine or "introspect" upon itself, and manipulate internal properties of the program. For example, it's possible for a Java class to obtain the names of all its members and display them.

根据这段描述来看,Reflection 之于 Java,主要是用于在程序运行时去获取和操作 Class 的 Fields 和 Methods 等用途。理解可能有些肤浅,毕竟好多年没写 Java 了。在我当年看来,觉得拥有 Reflection 对于 Java 这样的静态语言来说,实在是一个非常有用的大杀器,可以在程序运行的时候多了动态的能力。但在这里不表,我们主要还是讨论一下 JavaScript 的 Proxy 和 Reflect,关于 Java 的 Reflection,可以参考这篇文章。

提到 ProxyReflect ,就不得不提 Object.defineProperty,在我看来,它们是三个密切相关的语言特性,其都可以用某种方式影响 Object 的 property,getter 和 setter。

Definitions

为了深入的理解三者的含义、用途和适用范围,我们从 MDN 的定义开始入手,来研究一下三者的共性和不同。

Proxy

The Proxy object is used to define custom behavior for fundamental operations (e.g. property lookup, assignment, enumeration, function invocation, etc).

从上面的定义中我们可以看出,Proxy 是用来为对象的属性查找、赋值、枚举、函数调用等基本操作提供定制化的行为。按照 Proxy 的字面来理解,就是 Proxy 提供了一种代理上述基本操作的机制。

代理方式如下:

const p = new Proxy(target, handler);

针对要代理的目标 target,我们定义一个 p 作为代理,提供一个handler,后续所有需要代理的操作,都通过p 来进行,例如访问p.name。这种方式,有以下两个特点:

  • 创建 Proxy 时,不会改变 target 本身,返回的是一个全新的对象
  • 后续操作时,也全部通过操作 p 对象,来间接的操作 target,而不是直接操作 target

通过这种方式 p 成为了 target 的代理。(不知道为啥联想到了租房和卖房的中介😂

阮一峰老师在ECMAScript 6 入门中提到:

Proxy 实际上重载(overload)了点运算符,即用自己的定义覆盖了语言的原始定义。

针对这一点,我持保留意见。但由于在规范中没有找到相关的描述,只找到了关于 Property Accessors 的描述,暂时无法下定论。目前我的猜测是,访问 Proxy 对象的属性时,都会触发 handler 的 getter/setter,这个是 ECMAScript 2015 规范定义的 Proxy 自身的一种特性。也正因为这是一种全新的特性,与 Object 自身的 getter/setter 行为不一致,故无法真正的在只支持 ES5 的浏览器中去 polyfill,需要浏览器自身实现才能支持。

Reflect

Reflect is a built-in object that provides methods for interceptable JavaScript operations. The methods are the same as those of proxy handlers. Reflect is not a function object, so it's not constructible.

The Reflect object provides the following static functions which have the same names as the proxy handler methods. Some of these methods are the same as corresponding methods on Object.

Reflect 在 JavaScript 中的定位,我的理解是:

  • 将一些 Reflection 相关的函数,从 Object 上移到 Reflect 上部署
  • Reflect对象的方法与Proxy handler 的方法一一对应,极大的方便了在使用 Proxy 的时候,只用关心额外部署的功能,在其他 case 下优雅的 fallback
  • 同时,Reflect 提供的函数,让我们更安全的去操作 Object
Object.defineProperty

The Object.defineProperty() method defines a new property directly on an object, or modifies an existing property on an object, and returns the object.

This method allows precise addition to or modification of a property on an object.

defineProperty 提供了精确的添加/修改 object 属性的方法。在 ECMAScript 5.1 中被引入。

Usage

接下来,我们分别看一下,如何使用这三者,以及它们的适用场景。

Object.defineProperty
Object.defineProperty(obj, propName, propDescriptor);

如上面所示,用法很简洁。MDN 文档中有两个需要注意的点:

By default, values added using Object.defineProperty() are immutable.

Property descriptors present in objects come in two main flavors: data descriptors and accessor descriptors.

  • A data descriptor is a property that has a value, which may or may not be writable.
  • An accessor descriptor is a property described by a getter-setter pair of functions.
  • A descriptor must be one of these two flavors; it cannot be both.

文档说的很清楚:

  • 默认情况下,用 Object.defineProperty() 添加的属性和值是不可被改变的。
  • Property descriptors 分为 data descriptors and accessor descriptors 两种类型,一个 descriptor 只能是其中的一种类型,不可得兼。

接下来,我们分别看一下 data descriptors and accessor descriptors 的使用方法和异同。

data descriptors
const a = {};

Object.defineProperty(a, 'name', {
    configurable: false,    // default,shared
    enumerable: false,      // default,shared

    // 以下两个属性为 data descriptor only
    value: 'Jack',
    writable: false         // default
});

上面定义了一个 data descriptor。其中 configurableenumerable 是两种类型的 descriptors 都有的。

  • writable:value 值是否可以被改写,只由 writable 来决定。

    • 包括通过等号赋值和通过 Object.defineProperty 重新去覆盖 value。
    • 即使 configurable 为 false,只要 writable 为 true,依然可以通过 Object.defineProperty 来改写 value,并可以把 writable 从 true 改写为 false。
  • configurable:代表 property 是否可以被删除,以及其 descriptor 的属性是否可以被改写。
  • enumerable:代表 property 是否可以被 Object.keysfor...in 枚举。为 false 时,用 in 操作符,返回的依然是 true。因为这只代表 not enumerable,but not invisible。
accessor descriptors
let company;
Object.defineProperty(a, 'company', {
    get() {
        // do anything you want
        return company;
    },

    set(value) {
        // do anything you want
        company = value;
    }
});

这是 Vue 得以实现的基础。这里只贴个图,具体原理及源码,还请参考官网。

注意

  • 如果 descriptor 中同时包含 value/writable && getter/setter,在 defineProperty 的时候,将会 throw,“Cannot both specify accessors and a value or writable attribute”。
Proxy

之所以把 Object.defineProperty 的使用放在最前,是因为从语言的完备程度来讲,Object.defineProperty 提供了一种机制,让我们去定义一个对象上某个Propertygetter/setter ;而 Proxy 提供的机制是,通过设置一个拦截对象,去代理 target 对象所有 Propertygetter/setter。仅从作用上来说,等同于对 target 对象所有的 Property 设置了一个公用的 getter/setter,只不过这个 getter/setter 的参数略有不一样。但从实质上来说,Proxy 并未对 target 对象进行了改变,而是自身提供了一个拦截对象。

那么,会有如下两个疑问:

  • 对于 Proxy 提供的拦截对象使用 Object.defineProperty 会怎么样?
  • Proxy 是否可以被 polyfill?

带着这两个疑问,我们先看一下 Proxy 的基本用法。

有两种方式创建 Proxy,一种是通过 new 操作符,一种是通过 Proxy.revocable(),二者的区别在于后者创建的 Proxy 可以被 revoca。

const p = new Proxy(target, handler);

Proxy.revocable(target, handler);

target 很好理解,就是需要代理的对象。什么是一个 handler 呢?MDN 上是这样描述的:

The handler object is a placeholder object which contains traps for Proxy. All traps are optional. If a trap has not been defined, the default behavior is to forward the operation to the target.

handler 是一个包含一系列拦截函数的对象,所有的拦截函数都是可选的,如果一个拦截函数没有定义的话,就会使用 target 的默认行为。如何理解什么是 target 的默认行为?我们需要先了解一下,Proxy 都能代理些什么,即拦截函数包含哪些?

拦截函数可以按照其拦截的行为,分为三个类型:

Reflect

Reflect 提供的方法,跟上面列出的 Proxy 支持的 13 种函数一毛一样。主要还是提供给 Proxy,在不需要额外处理的 case 下,作为 fallback。

Refs

下一篇《How nginx processes a request》