# Proxy 和 Reflect

# Proxy

# 简介

Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种“元编程”(meta programming),即对编程语言进行编程。

Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。

var proxy = new Proxy(target, handler);
1

new Proxy() 表示生成一个 Proxy 实例,target 参数表示所要拦截的目标对象(包括函数),handler 参数也是一个对象,用来定制拦截行为。

var proxy = new Proxy({}, {
  get: function(target, propKey) {
    return 35;
  }
});

proxy.time // 35
proxy.name // 35
proxy.title // 35
1
2
3
4
5
6
7
8
9

要使得Proxy起作用,必须针对Proxy实例(上例是proxy对象)进行操作,而不是针对目标对象(上例是空对象)进行操作。

# Proxy 实例的方法

# get(target, propKey, receiver)

拦截对象属性的读取,比如 proxy.fooproxy['foo']。如果一个属性不可配置(configurable)且不可写(writable),则 Proxy 不能修改该属性,通过 Proxy 对象访问该属性会报错。

# set(target, propKey, value, receiver)

拦截对象属性的设置,比如 proxy.foo = vproxy['foo'] = v,返回一个布尔值。

set 方法用来拦截某个属性的赋值操作,可以接受四个参数,依次为目标对象、属性名、属性值和 Proxy 实例本身,其中最后一个参数可选。如果目标对象自身的某个属性不可写,那么 set 方法将不起作用。严格模式下,set 代理如果没有返回 true,就会报错。

# has(target, propKey)

拦截 propKey in proxy 的操作,返回一个布尔值。用来拦截 HasProperty 操作,即判断对象是否具有某个属性时,这个方法会生效。典型的操作就是 in 运算符。has()方法可以接受两个参数,分别是目标对象、需查询的属性名。has() 方法拦截的是 HasProperty 操作,而不是 HasOwnProperty 操作,即 has() 方法不判断一个属性是对象自身的属性,还是继承的属性。另外,虽然 for...in 循环也用到了 in 运算符,但是 has() 拦截对 for...in 循环不生效。

# deleteProperty(target, propKey)

拦截 delete proxy[propKey] 的操作,返回一个布尔值。

# ownKeys(target)

拦截 Object.getOwnPropertyNames(proxy)Object.getOwnPropertySymbols(proxy)Object.keys(proxy)for...in循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而 Object.keys() 的返回结果仅包括目标对象自身的可遍历属性。

# getOwnPropertyDescriptor(target, propKey)

拦截 Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。

# defineProperty(target, propKey, propDesc)

拦截 Object.defineProperty(proxy, propKey, propDesc)Object.defineProperties(proxy, propDescs),返回一个布尔值。

# preventExtensions(target)

拦截 Object.preventExtensions(proxy),返回一个布尔值。

# getPrototypeOf(target)

拦截 Object.getPrototypeOf(proxy),返回一个对象。

# isExtensible(target)

拦截 Object.isExtensible(proxy),返回一个布尔值。

# setPrototypeOf(target, proto)

拦截 Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。

# apply(target, object, args)

拦截 Proxy 实例作为函数调用的操作,比如 proxy(...args)proxy.call(object, ...args)proxy.apply(...)。apply 方法可以接受三个参数,分别是目标对象、目标对象的上下文对象(this)和目标对象的参数数组。

# construct(target, args)

拦截 Proxy 实例作为构造函数调用的操作,比如 new proxy(...args)。 construct() 方法用于拦截 new 命令。

# Proxy 实例的方法一览表

内部方法 Handler 方法 何时触发
[[Get]] get 读取属性
[[Set]] set 写入属性
[[HasProperty]] has in 运算符
[[Delete]] deleteProperty delete 操作
[[Call]] apply proxy 对象作为函数被调用
[[Construct]] construct new 操作
[[GetPrototypeOf]] getPrototypeOf Object.getPrototypeOf (opens new window)
[[SetPrototypeOf]] setPrototypeOf Object.setPrototypeOf (opens new window)
[[IsExtensible]] isExtensible Object.isExtensible (opens new window)
[[PreventExtensions]] preventExtensions Object.preventExtensions (opens new window)
[[DefineOwnProperty]] defineProperty Object.defineProperty (opens new window), Object.defineProperties (opens new window)
[[GetOwnProperty]] getOwnPropertyDescriptor Object.getOwnPropertyDescriptor (opens new window), for..in, Object.keys/values/entries
[[OwnPropertyKeys]] ownKeys Object.getOwnPropertyNames (opens new window), Object.getOwnPropertySymbols (opens new window), for..in, Object/keys/values/entries

# definePropety 与 proxy

# definePropety

ES5 提供了 Object.defineProperty 方法,该方法可以在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回这个对象。

Object.defineProperty(obj, prop, descriptor)

obj: 要在其上定义属性的对象。
prop:  要定义或修改的属性的名称。
descriptor: 将被定义或修改的属性的描述符。

// 例如
var obj = {};
Object.defineProperty(obj, "num", {
    value : 1,
    writable : true,
    enumerable : true,
    configurable : true
});
//  对象 obj 拥有属性 num,值为 1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

虽然我们可以直接添加属性和值,但是使用这种方式,我们能进行更多的配置。

函数的第三个参数 descriptor 所表示的属性描述符有两种形式:数据描述符和存取描述符

两者均具有以下两种键值:

  • configurable 当且仅当该属性的 configurable 为 true 时,该属性描述符才能够被改变,也能够被删除。默认为 false。
  • enumerable 当且仅当该属性的 enumerable 为 true 时,该属性才能够出现在对象的枚举属性中。默认为 false。 数据描述符同时具有以下可选键值:
  • value 该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。默认为 undefined。
  • writable 当且仅当该属性的 writable 为 true 时,该属性才能被赋值运算符改变。默认为 false。 存取描述符同时具有以下可选键值:
  • get 一个给属性提供 getter 的方法,如果没有 getter 则为 undefined。该方法返回值被用作属性值。默认为 undefined。
  • set 一个给属性提供 setter 的方法,如果没有 setter 则为 undefined。该方法将接受唯一参数,并将该参数的新值分配给该属性。默认为 undefined。 值得注意的是:

属性描述符必须是数据描述符或者存取描述符两种形式之一,不能同时是两者 。这就意味着你可以:

Object.defineProperty({}, "num", {
    value: 1,
    writable: true,
    enumerable: true,
    configurable: true
});
1
2
3
4
5
6

也可以:

var value = 1;
Object.defineProperty({}, "num", {
    get : function(){
      return value;
    },
    set : function(newValue){
      value = newValue;
    },
    enumerable : true,
    configurable : true
});
1
2
3
4
5
6
7
8
9
10
11

但是不可以:

// 报错
Object.defineProperty({}, "num", {
    value: 1,
    get: function() {
        return 1;
    }
});
1
2
3
4
5
6
7

此外,所有的属性描述符都是非必须的,但是 descriptor 这个字段是必须的,如果不进行任何配置,你可以这样:

var obj = Object.defineProperty({}, "num", {});
console.log(obj.num); // undefined
1
2

# Setters 和 Getters

之所以讲到 defineProperty,是因为我们要使用存取描述符中的 get 和 set,这两个方法又被称为 getter 和 setter。由 getter 和 setter 定义的属性称做”存取器属性“。

当程序查询存取器属性的值时,JavaScript 调用 getter方法。这个方法的返回值就是属性存取表达式的值。当程序设置一个存取器属性的值时,JavaScript 调用 setter 方法,将赋值表达式右侧的值当做参数传入 setter。从某种意义上讲,这个方法负责“设置”属性值。可以忽略 setter 方法的返回值。

举个例子:

var obj = {}, value = null;
Object.defineProperty(obj, "num", {
    get: function(){
        console.log('执行了 get 操作')
        return value;
    },
    set: function(newValue) {
        console.log('执行了 set 操作')
        value = newValue;
    }
})

obj.num = 1 // 执行了 set 操作

console.log(obj.num); // 执行了 get 操作 // 1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# proxy

使用 defineProperty 只能重定义属性的读取(get)和设置(set)行为,到了 ES6,提供了 Proxy,可以重定义更多的行为,比如 in、delete、函数调用等更多行为。

使用 proxy 再来写一下 watch 函数:

(function() {
    var root = this;

    function watch(target, func) {

        var proxy = new Proxy(target, {
            get: function(target, prop) {
                return target[prop];
            },
            set: function(target, prop, value) {
                target[prop] = value;
                func(prop, value);
            }
        });

        return proxy;
    }

    this.watch = watch;
})()

var obj = {
    value: 1
}

var newObj = watch(obj, function(key, newvalue) {
    if (key == 'value') document.getElementById('container').innerHTML = newvalue;
})

document.getElementById('button').addEventListener("click", function() {
    newObj.value += 1
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

我们也可以发现,使用 definePropertyproxy 的区别,当使用 defineProperty,我们修改原来的 obj 对象就可以触发拦截,而使用 proxy,就必须修改代理对象,即 Proxy 的实例才可以触发拦截。

# Reflect

Reflect 对象与 Proxy 对象一样,也是 ES6 为了操作对象而提供的新 API。Reflect 不是一个函数对象,因此它是不可构造的。

  1. 将 Object 对象的一些明显属于 语言内部 的方法(比如 Object.defineProperty),放到 Reflect 对象上。现阶段,某些方法同时在 Object 和 Reflect 对象上部署,未来的新方法将只部署在 Reflect 对象上。也就是说,从 Reflect 对象上可以拿到语言内部的方法。
  2. 修改某些 Object 方法的返回结果,让其变得更合理。比如,Object.defineProperty(obj, name, desc) 在无法定义属性时,会抛出一个错误,而 Reflect.defineProperty(obj, name, desc) 则会返回 false。
  3. 让 Object 操作都变成函数行为。某些 Object 操作是命令式,比如 name in objdelete obj[name],而 Reflect.has(obj, name)Reflect.deleteProperty(obj, name) 让它们变成了函数行为。
  4. Reflect 对象的方法与 Proxy 对象的方法一一对应,只要是 Proxy 对象的方法,就能在 Reflect 对象上找到对应的方法。这就让 Proxy 对象可以方便地调用对应的 Reflect 方法,完成默认行为,作为修改行为的基础。也就是说,不管 Proxy 怎么修改默认行为,你总可以在Reflect 上获取默认行为。

# Reflect 静态方法

Reflect对象一共有 13 个静态方法。

  • Reflect.apply(target, thisArg, args)
  • Reflect.construct(target, args)
  • Reflect.get(target, name, receiver)
  • Reflect.set(target, name, value, receiver)
  • Reflect.defineProperty(target, name, desc)
  • Reflect.deleteProperty(target, name)
  • Reflect.has(target, name)
  • Reflect.ownKeys(target)
  • Reflect.isExtensible(target)
  • Reflect.preventExtensions(target)
  • Reflect.getOwnPropertyDescriptor(target, name)
  • Reflect.getPrototypeOf(target)
  • Reflect.setPrototypeOf(target, prototype)