# Proxy 和 Reflect
# Proxy
# 简介
Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种“元编程”(meta programming),即对编程语言进行编程。
Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。
var proxy = new Proxy(target, handler);
new Proxy() 表示生成一个 Proxy 实例,target 参数表示所要拦截的目标对象(包括函数),handler 参数也是一个对象,用来定制拦截行为。
var proxy = new Proxy({}, {
get: function(target, propKey) {
return 35;
}
});
proxy.time // 35
proxy.name // 35
proxy.title // 35
2
3
4
5
6
7
8
9
要使得Proxy
起作用,必须针对Proxy
实例(上例是proxy
对象)进行操作,而不是针对目标对象(上例是空对象)进行操作。
# Proxy 实例的方法
# get(target, propKey, receiver)
拦截对象属性的读取,比如 proxy.foo
和 proxy['foo']
。如果一个属性不可配置(configurable)且不可写(writable),则 Proxy 不能修改该属性,通过 Proxy 对象访问该属性会报错。
# set(target, propKey, value, receiver)
拦截对象属性的设置,比如 proxy.foo = v
或 proxy['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
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
});
2
3
4
5
6
也可以:
var value = 1;
Object.defineProperty({}, "num", {
get : function(){
return value;
},
set : function(newValue){
value = newValue;
},
enumerable : true,
configurable : true
});
2
3
4
5
6
7
8
9
10
11
但是不可以:
// 报错
Object.defineProperty({}, "num", {
value: 1,
get: function() {
return 1;
}
});
2
3
4
5
6
7
此外,所有的属性描述符都是非必须的,但是 descriptor 这个字段是必须的,如果不进行任何配置,你可以这样:
var obj = Object.defineProperty({}, "num", {});
console.log(obj.num); // undefined
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
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
});
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
我们也可以发现,使用 defineProperty
和 proxy
的区别,当使用 defineProperty
,我们修改原来的 obj 对象就可以触发拦截,而使用 proxy,就必须修改代理对象,即 Proxy 的实例才可以触发拦截。
# Reflect
Reflect 对象与 Proxy 对象一样,也是 ES6 为了操作对象而提供的新 API。Reflect 不是一个函数对象,因此它是不可构造的。
- 将 Object 对象的一些明显属于 语言内部 的方法(比如
Object.defineProperty
),放到 Reflect 对象上。现阶段,某些方法同时在 Object 和 Reflect 对象上部署,未来的新方法将只部署在 Reflect 对象上。也就是说,从 Reflect 对象上可以拿到语言内部的方法。 - 修改某些 Object 方法的返回结果,让其变得更合理。比如,
Object.defineProperty(obj, name, desc)
在无法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj, name, desc)
则会返回 false。 - 让 Object 操作都变成函数行为。某些 Object 操作是命令式,比如
name in obj
和delete obj[name]
,而Reflect.has(obj, name)
和Reflect.deleteProperty(obj, name)
让它们变成了函数行为。 - 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)