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
要使得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 |
[[SetPrototypeOf]] | setPrototypeOf | Object.setPrototypeOf |
[[IsExtensible]] | isExtensible | Object.isExtensible |
[[PreventExtensions]] | preventExtensions | Object.preventExtensions |
[[DefineOwnProperty]] | defineProperty | Object.defineProperty, Object.defineProperties |
[[GetOwnProperty]] | getOwnPropertyDescriptor | Object.getOwnPropertyDescriptor, for..in , Object.keys/values/entries |
[[OwnPropertyKeys]] | ownKeys | Object.getOwnPropertyNames, Object.getOwnPropertySymbols, 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
虽然我们可以直接添加属性和值,但是使用这种方式,我们能进行更多的配置。
函数的第三个参数 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
});
也可以:
var value = 1;
Object.defineProperty({}, "num", {
get : function(){
return value;
},
set : function(newValue){
value = newValue;
},
enumerable : true,
configurable : true
});
但是不可以:
// 报错
Object.defineProperty({}, "num", {
value: 1,
get: function() {
return 1;
}
});
此外,所有的属性描述符都是非必须的,但是 descriptor 这个字段是必须的,如果不进行任何配置,你可以这样:
var obj = Object.defineProperty({}, "num", {});
console.log(obj.num); // undefined
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
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
});
我们也可以发现,使用 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)