1
0
mirror of https://github.com/ruanyf/es6tutorial.git synced 2025-05-24 18:32:22 +00:00
es6tutorial/docs/proxy.md
2016-12-19 23:13:30 +08:00

1338 lines
36 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Proxy 和 Reflect
## Proxy 概述
Proxy 用于修改某些操作的默认行为等同于在语言层面做出修改所以属于一种“元编程”meta programming即对编程语言进行编程。
Proxy 可以理解成在目标对象之前架设一层“拦截”外界对该对象的访问都必须先通过这层拦截因此提供了一种机制可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。
```javascript
var obj = new Proxy({}, {
get: function (target, key, receiver) {
console.log(`getting ${key}!`);
return Reflect.get(target, key, receiver);
},
set: function (target, key, value, receiver) {
console.log(`setting ${key}!`);
return Reflect.set(target, key, value, receiver);
}
});
```
上面代码对一个空对象架设了一层拦截,重定义了属性的读取(`get`)和设置(`set`)行为。这里暂时先不解释具体的语法,只看运行结果。对设置了拦截行为的对象`obj`,去读写它的属性,就会得到下面的结果。
```javascript
obj.count = 1
// setting count!
++obj.count
// getting count!
// setting count!
// 2
```
上面代码说明Proxy 实际上重载overload了点运算符即用自己的定义覆盖了语言的原始定义。
ES6 原生提供 Proxy 构造函数,用来生成 Proxy 实例。
```javascript
var proxy = new Proxy(target, handler);
```
Proxy 对象的所有用法,都是上面这种形式,不同的只是`handler`参数的写法。其中,`new Proxy()`表示生成一个`Proxy`实例,`target`参数表示所要拦截的目标对象,`handler`参数也是一个对象,用来定制拦截行为。
下面是另一个拦截读取属性行为的例子。
```javascript
var proxy = new Proxy({}, {
get: function(target, property) {
return 35;
}
});
proxy.time // 35
proxy.name // 35
proxy.title // 35
```
上面代码中,作为构造函数,`Proxy`接受两个参数。第一个参数是所要代理的目标对象(上例是一个空对象),即如果没有`Proxy`的介入,操作原来要访问的就是这个对象;第二个参数是一个配置对象,对于每一个被代理的操作,需要提供一个对应的处理函数,该函数将拦截对应的操作。比如,上面代码中,配置对象有一个`get`方法,用来拦截对目标对象属性的访问请求。`get`方法的两个参数分别是目标对象和所要访问的属性。可以看到,由于拦截函数总是返回`35`,所以访问任何属性都得到`35`
注意,要使得`Proxy`起作用,必须针对`Proxy`实例(上例是`proxy`对象)进行操作,而不是针对目标对象(上例是空对象)进行操作。
如果`handler`没有设置任何拦截,那就等同于直接通向原对象。
```javascript
var target = {};
var handler = {};
var proxy = new Proxy(target, handler);
proxy.a = 'b';
target.a // "b"
```
上面代码中,`handler`是一个空对象,没有任何拦截效果,访问`handler`就等同于访问`target`
一个技巧是将Proxy对象设置到`object.proxy`属性,从而可以在`object`对象上调用。
```javascript
var object = { proxy: new Proxy(target, handler) };
```
Proxy 实例也可以作为其他对象的原型对象。
```javascript
var proxy = new Proxy({}, {
get: function(target, property) {
return 35;
}
});
let obj = Object.create(proxy);
obj.time // 35
```
上面代码中,`proxy`对象是`obj`对象的原型,`obj`对象本身并没有`time`属性,所以根据原型链,会在`proxy`对象上读取该属性,导致被拦截。
同一个拦截器函数,可以设置拦截多个操作。
```javascript
var handler = {
get: function(target, name) {
if (name === 'prototype') {
return Object.prototype;
}
return 'Hello, ' + name;
},
apply: function(target, thisBinding, args) {
return args[0];
},
construct: function(target, args) {
return {value: args[1]};
}
};
var fproxy = new Proxy(function(x, y) {
return x + y;
}, handler);
fproxy(1, 2) // 1
new fproxy(1,2) // {value: 2}
fproxy.prototype === Object.prototype // true
fproxy.foo // "Hello, foo"
```
下面是 Proxy 支持的拦截操作一览。
对于可以设置、但没有设置拦截的操作,则直接落在目标对象上,按照原先的方式产生结果。
**1get(target, propKey, receiver)**
拦截对象属性的读取,比如`proxy.foo``proxy['foo']`
最后一个参数`receiver`是一个对象,可选,参见下面`Reflect.get`的部分。
**2set(target, propKey, value, receiver)**
拦截对象属性的设置,比如`proxy.foo = v``proxy['foo'] = v`,返回一个布尔值。
**3has(target, propKey)**
拦截`propKey in proxy`的操作,以及对象的`hasOwnProperty`方法,返回一个布尔值。
**4deleteProperty(target, propKey)**
拦截`delete proxy[propKey]`的操作,返回一个布尔值。
**5ownKeys(target)**
拦截`Object.getOwnPropertyNames(proxy)``Object.getOwnPropertySymbols(proxy)``Object.keys(proxy)`,返回一个数组。该方法返回对象所有自身的属性,而`Object.keys()`仅返回对象可遍历的属性。
**6getOwnPropertyDescriptor(target, propKey)**
拦截`Object.getOwnPropertyDescriptor(proxy, propKey)`,返回属性的描述对象。
**7defineProperty(target, propKey, propDesc)**
拦截`Object.defineProperty(proxy, propKey, propDesc``Object.defineProperties(proxy, propDescs)`,返回一个布尔值。
**8preventExtensions(target)**
拦截`Object.preventExtensions(proxy)`,返回一个布尔值。
**9getPrototypeOf(target)**
拦截`Object.getPrototypeOf(proxy)`,返回一个对象。
**10isExtensible(target)**
拦截`Object.isExtensible(proxy)`,返回一个布尔值。
**11setPrototypeOf(target, proto)**
拦截`Object.setPrototypeOf(proxy, proto)`,返回一个布尔值。
如果目标对象是函数,那么还有两种额外操作可以拦截。
**12apply(target, object, args)**
拦截 Proxy 实例作为函数调用的操作,比如`proxy(...args)``proxy.call(object, ...args)``proxy.apply(...)`
**13construct(target, args)**
拦截 Proxy 实例作为构造函数调用的操作,比如`new proxy(...args)`
## Proxy实例的方法
下面是上面这些拦截方法的详细介绍。
### get()
`get`方法用于拦截某个属性的读取操作。上文已经有一个例子,下面是另一个拦截读取操作的例子。
```javascript
var person = {
name: "张三"
};
var proxy = new Proxy(person, {
get: function(target, property) {
if (property in target) {
return target[property];
} else {
throw new ReferenceError("Property \"" + property + "\" does not exist.");
}
}
});
proxy.name // "张三"
proxy.age // 抛出一个错误
```
上面代码表示,如果访问目标对象不存在的属性,会抛出一个错误。如果没有这个拦截函数,访问不存在的属性,只会返回`undefined`
`get`方法可以继承。
```javascript
let proto = new Proxy({}, {
get(target, propertyKey, receiver) {
console.log('GET '+propertyKey);
return target[propertyKey];
}
});
let obj = Object.create(proto);
obj.xxx // "GET xxx"
```
上面代码中,拦截操作定义在`Prototype`对象上面,所以如果读取`obj`对象继承的属性时,拦截会生效。
下面的例子使用`get`拦截,实现数组读取负数的索引。
```javascript
function createArray(...elements) {
let handler = {
get(target, propKey, receiver) {
let index = Number(propKey);
if (index < 0) {
propKey = String(target.length + index);
}
return Reflect.get(target, propKey, receiver);
}
};
let target = [];
target.push(...elements);
return new Proxy(target, handler);
}
let arr = createArray('a', 'b', 'c');
arr[-1] // c
```
上面代码中,数组的位置参数是`-1`,就会输出数组的倒数最后一个成员。
利用 Proxy可以将读取属性的操作`get`),转变为执行某个函数,从而实现属性的链式操作。
```javascript
var pipe = (function () {
return function (value) {
var funcStack = [];
var oproxy = new Proxy({} , {
get : function (pipeObject, fnName) {
if (fnName === 'get') {
return funcStack.reduce(function (val, fn) {
return fn(val);
},value);
}
funcStack.push(window[fnName]);
return oproxy;
}
});
return oproxy;
}
}());
var double = n => n * 2;
var pow = n => n * n;
var reverseInt = n => n.toString().split("").reverse().join("") | 0;
pipe(3).double.pow.reverseInt.get; // 63
```
上面代码设置 Proxy 以后,达到了将函数名链式使用的效果。
下面的例子则是利用`get`拦截实现一个生成各种DOM节点的通用函数`dom`
```javascript
const dom = new Proxy({}, {
get(target, property) {
return function(attrs = {}, ...children) {
const el = document.createElement(property);
for (let prop of Object.keys(attrs)) {
el.setAttribute(prop, attrs[prop]);
}
for (let child of children) {
if (typeof child === 'string') {
child = document.createTextNode(child);
}
el.appendChild(child);
}
return el;
}
}
});
const el = dom.div({},
'Hello, my name is ',
dom.a({href: '//example.com'}, 'Mark'),
'. I like:',
dom.ul({},
dom.li({}, 'The web'),
dom.li({}, 'Food'),
dom.li({}, '…actually that\'s it')
)
);
document.body.appendChild(el);
```
### set()
`set`方法用来拦截某个属性的赋值操作。
假定`Person`对象有一个`age`属性该属性应该是一个不大于200的整数那么可以使用`Proxy`保证`age`的属性值符合要求。
```javascript
let validator = {
set: function(obj, prop, value) {
if (prop === 'age') {
if (!Number.isInteger(value)) {
throw new TypeError('The age is not an integer');
}
if (value > 200) {
throw new RangeError('The age seems invalid');
}
}
// 对于age以外的属性直接保存
obj[prop] = value;
}
};
let person = new Proxy({}, validator);
person.age = 100;
person.age // 100
person.age = 'young' // 报错
person.age = 300 // 报错
```
上面代码中,由于设置了存值函数`set`,任何不符合要求的`age`属性赋值,都会抛出一个错误。利用`set`方法还可以数据绑定即每当对象发生变化时会自动更新DOM。
有时,我们会在对象上面设置内部属性,属性名的第一个字符使用下划线开头,表示这些属性不应该被外部使用。结合`get``set`方法,就可以做到防止这些内部属性被外部读写。
```javascript
var handler = {
get (target, key) {
invariant(key, 'get');
return target[key];
},
set (target, key, value) {
invariant(key, 'set');
target[key] = value;
return true;
}
};
function invariant (key, action) {
if (key[0] === '_') {
throw new Error(`Invalid attempt to ${action} private "${key}" property`);
}
}
var target = {};
var proxy = new Proxy(target, handler);
proxy._prop
// Error: Invalid attempt to get private "_prop" property
proxy._prop = 'c'
// Error: Invalid attempt to set private "_prop" property
```
上面代码中,只要读写的属性名的第一个字符是下划线,一律抛错,从而达到禁止读写内部属性的目的。
### apply()
`apply`方法拦截函数的调用、call和apply操作。
```javascript
var handler = {
apply (target, ctx, args) {
return Reflect.apply(...arguments);
}
};
```
`apply`方法可以接受三个参数,分别是目标对象、目标对象的上下文对象(`this`)和目标对象的参数数组。
下面是一个例子。
```javascript
var target = function () { return 'I am the target'; };
var handler = {
apply: function () {
return 'I am the proxy';
}
};
var p = new Proxy(target, handler);
p()
// "I am the proxy"
```
上面代码中,变量`p`是Proxy的实例当它作为函数调用时`p()`),就会被`apply`方法拦截,返回一个字符串。
下面是另外一个例子。
```javascript
var twice = {
apply (target, ctx, args) {
return Reflect.apply(...arguments) * 2;
}
};
function sum (left, right) {
return left + right;
};
var proxy = new Proxy(sum, twice);
proxy(1, 2) // 6
proxy.call(null, 5, 6) // 22
proxy.apply(null, [7, 8]) // 30
```
上面代码中,每当执行`proxy`函数(直接调用或`call``apply`调用),就会被`apply`方法拦截。
另外,直接调用`Reflect.apply`方法,也会被拦截。
```javascript
Reflect.apply(proxy, null, [9, 10]) // 38
```
### has()
`has`方法用来拦截`HasProperty`操作,即判断对象是否具有某个属性时,这个方法会生效。典型的操作就是`in`运算符。
下面的例子使用`has`方法隐藏某些属性,不被`in`运算符发现。
```javascript
var handler = {
has (target, key) {
if (key[0] === '_') {
return false;
}
return key in target;
}
};
var target = { _prop: 'foo', prop: 'foo' };
var proxy = new Proxy(target, handler);
'_prop' in proxy // false
```
上面代码中,如果原对象的属性名的第一个字符是下划线,`proxy.has`就会返回`false`,从而不会被`in`运算符发现。
如果原对象不可配置或者禁止扩展,这时`has`拦截会报错。
```javascript
var obj = { a: 10 };
Object.preventExtensions(obj);
var p = new Proxy(obj, {
has: function(target, prop) {
return false;
}
});
'a' in p // TypeError is thrown
```
上面代码中,`obj`对象禁止扩展,结果使用`has`拦截就会报错。
值得注意的是,`has`方法拦截的是`HasProperty`操作,而不是`HasOwnProperty`操作,即`has`方法不判断一个属性是对象自身的属性,还是继承的属性。
另外,虽然`for...in`循环也用到了`in`运算符,但是`has`拦截对`for...in`循环不生效。
```javascript
let stu1 = {name: '张三', score: 59};
let stu2 = {name: '李四', score: 99};
let handler = {
has(target, prop) {
if (prop === 'score' && target[prop] < 60) {
console.log(`${target.name} 不及格`);
return false;
}
return prop in target;
}
}
let oproxy1 = new Proxy(stu1, handler);
let oproxy2 = new Proxy(stu2, handler);
'score' in oproxy1
// 张三 不及格
// false
'score' in oproxy2
// true
for (let a in oproxy1) {
console.log(oproxy1[a]);
}
// 张三
// 59
for (let b in oproxy2) {
console.log(oproxy2[b]);
}
// 李四
// 99
```
上面代码中,`has`拦截只对`in`循环生效,对`for...in`循环不生效,导致不符合要求的属性没有被排除在`for...in`循环之外。
### construct()
`construct`方法用于拦截`new`命令,下面是拦截对象的写法。
```javascript
var handler = {
construct (target, args, newTarget) {
return new target(...args);
}
};
```
`construct`方法可以接受两个参数。
- `target`: 目标对象
- `args`:构建函数的参数对象
下面是一个例子。
```javascript
var p = new Proxy(function() {}, {
construct: function(target, args) {
console.log('called: ' + args.join(', '));
return { value: args[0] * 10 };
}
});
new p(1).value
// "called: 1"
// 10
```
`construct`方法返回的必须是一个对象,否则会报错。
```javascript
var p = new Proxy(function() {}, {
construct: function(target, argumentsList) {
return 1;
}
});
new p() // 报错
```
### deleteProperty()
`deleteProperty`方法用于拦截`delete`操作,如果这个方法抛出错误或者返回`false`,当前属性就无法被`delete`命令删除。
```javascript
var handler = {
deleteProperty (target, key) {
invariant(key, 'delete');
return true;
}
};
function invariant (key, action) {
if (key[0] === '_') {
throw new Error(`Invalid attempt to ${action} private "${key}" property`);
}
}
var target = { _prop: 'foo' };
var proxy = new Proxy(target, handler);
delete proxy._prop
// Error: Invalid attempt to delete private "_prop" property
```
上面代码中,`deleteProperty`方法拦截了`delete`操作符,删除第一个字符为下划线的属性会报错。
### defineProperty()
`defineProperty`方法拦截了`Object.defineProperty`操作。
```javascript
var handler = {
defineProperty (target, key, descriptor) {
return false;
}
};
var target = {};
var proxy = new Proxy(target, handler);
proxy.foo = 'bar'
// TypeError: proxy defineProperty handler returned false for property '"foo"'
```
上面代码中,`defineProperty`方法返回`false`,导致添加新属性会抛出错误。
### getOwnPropertyDescriptor()
`getOwnPropertyDescriptor`方法拦截`Object.getOwnPropertyDescriptor`,返回一个属性描述对象或者`undefined`
```javascript
var handler = {
getOwnPropertyDescriptor (target, key) {
if (key[0] === '_') {
return;
}
return Object.getOwnPropertyDescriptor(target, key);
}
};
var target = { _foo: 'bar', baz: 'tar' };
var proxy = new Proxy(target, handler);
Object.getOwnPropertyDescriptor(proxy, 'wat')
// undefined
Object.getOwnPropertyDescriptor(proxy, '_foo')
// undefined
Object.getOwnPropertyDescriptor(proxy, 'baz')
// { value: 'tar', writable: true, enumerable: true, configurable: true }
```
上面代码中,`handler.getOwnPropertyDescriptor`方法对于第一个字符为下划线的属性名会返回`undefined`
### getPrototypeOf()
`getPrototypeOf`方法主要用来拦截`Object.getPrototypeOf()`运算符,以及其他一些操作。
- `Object.prototype.__proto__`
- `Object.prototype.isPrototypeOf()`
- `Object.getPrototypeOf()`
- `Reflect.getPrototypeOf()`
- `instanceof`运算符
下面是一个例子。
```javascript
var proto = {};
var p = new Proxy({}, {
getPrototypeOf(target) {
return proto;
}
});
Object.getPrototypeOf(p) === proto // true
```
上面代码中,`getPrototypeOf`方法拦截`Object.getPrototypeOf()`,返回`proto`对象。
### isExtensible()
`isExtensible`方法拦截`Object.isExtensible`操作。
```javascript
var p = new Proxy({}, {
isExtensible: function(target) {
console.log("called");
return true;
}
});
Object.isExtensible(p)
// "called"
// true
```
上面代码设置了`isExtensible`方法,在调用`Object.isExtensible`时会输出`called`
这个方法有一个强限制,如果不能满足下面的条件,就会抛出错误。
```javascript
Object.isExtensible(proxy) === Object.isExtensible(target)
```
下面是一个例子。
```javascript
var p = new Proxy({}, {
isExtensible: function(target) {
return false;
}
});
Object.isExtensible(p) // 报错
```
### ownKeys()
`ownKeys`方法用来拦截`Object.keys()`操作。
```javascript
let target = {};
let handler = {
ownKeys(target) {
return ['hello', 'world'];
}
};
let proxy = new Proxy(target, handler);
Object.keys(proxy)
// [ 'hello', 'world' ]
```
上面代码拦截了对于`target`对象的`Object.keys()`操作,返回预先设定的数组。
下面的例子是拦截第一个字符为下划线的属性名。
```javascript
let target = {
_bar: 'foo',
_prop: 'bar',
prop: 'baz'
};
let handler = {
ownKeys (target) {
return Reflect.ownKeys(target).filter(key => key[0] !== '_');
}
};
let proxy = new Proxy(target, handler);
for (let key of Object.keys(proxy)) {
console.log(target[key]);
}
// "baz"
```
### preventExtensions()
`preventExtensions`方法拦截`Object.preventExtensions()`。该方法必须返回一个布尔值。
这个方法有一个限制,只有当`Object.isExtensible(proxy)``false`(即不可扩展)时,`proxy.preventExtensions`才能返回`true`,否则会报错。
```javascript
var p = new Proxy({}, {
preventExtensions: function(target) {
return true;
}
});
Object.preventExtensions(p) // 报错
```
上面代码中,`proxy.preventExtensions`方法返回`true`,但这时`Object.isExtensible(proxy)`会返回`true`,因此报错。
为了防止出现这个问题,通常要在`proxy.preventExtensions`方法里面,调用一次`Object.preventExtensions`
```javascript
var p = new Proxy({}, {
preventExtensions: function(target) {
console.log("called");
Object.preventExtensions(target);
return true;
}
});
Object.preventExtensions(p)
// "called"
// true
```
### setPrototypeOf()
`setPrototypeOf`方法主要用来拦截`Object.setPrototypeOf`方法。
下面是一个例子。
```javascript
var handler = {
setPrototypeOf (target, proto) {
throw new Error('Changing the prototype is forbidden');
}
};
var proto = {};
var target = function () {};
var proxy = new Proxy(target, handler);
proxy.setPrototypeOf(proxy, proto);
// Error: Changing the prototype is forbidden
```
上面代码中,只要修改`target`的原型对象,就会报错。
## Proxy.revocable()
Proxy.revocable方法返回一个可取消的Proxy实例。
```javascript
let target = {};
let handler = {};
let {proxy, revoke} = Proxy.revocable(target, handler);
proxy.foo = 123;
proxy.foo // 123
revoke();
proxy.foo // TypeError: Revoked
```
`Proxy.revocable`方法返回一个对象,该对象的`proxy`属性是`Proxy`实例,`revoke`属性是一个函数,可以取消`Proxy`实例。上面代码中,当执行`revoke`函数之后,再访问`Proxy`实例,就会抛出一个错误。
## this 问题
虽然 Proxy 可以代理针对目标对象的访问,但它不是目标对象的透明代理,即不做任何拦截的情况下,也无法保证与目标对象的行为一致。主要原因就是在 Proxy 代理的情况下,目标对象内部的`this`关键字会指向 Proxy 代理。
```javascript
const target = {
m: function () {
console.log(this === proxy);
}
};
const handler = {};
const proxy = new Proxy(target, handler);
target.m() // false
proxy.m() // true
```
上面代码中,一旦`proxy`代理`target.m`,后者内部的`this`就是指向`proxy`,而不是`target`
下面是一个例子,由于`this`指向的变化,导致 Proxy 无法代理目标对象。
```javascript
const _name = new WeakMap();
class Person {
constructor(name) {
_name.set(this, name);
}
get name() {
return _name.get(this);
}
}
const jane = new Person('Jane');
jane.name // 'Jane'
const proxy = new Proxy(jane, {});
proxy.name // undefined
```
上面代码中,目标对象`jane``name`属性,实际保存在外部`WeakMap`对象`_name`上面,通过`this`键区分。由于通过`proxy.name`访问时,`this`指向`proxy`,导致无法取到值,所以返回`undefined`
此外,有些原生对象的内部属性,只有通过正确的`this`才能拿到,所以 Proxy 也无法代理这些原生对象的属性。
```javascript
const target = new Date();
const handler = {};
const proxy = new Proxy(target, handler);
proxy.getDate();
// TypeError: this is not a Date object.
```
上面代码中,`getDate`方法只能在`Date`对象实例上面拿到,如果`this`不是`Date`对象实例就会报错。这时,`this`绑定原始对象,就可以解决这个问题。
```javascript
const target = new Date('2015-01-01');
const handler = {
get(target, prop) {
if (prop === 'getDate') {
return target.getDate.bind(target);
}
return Reflect.get(target, prop);
}
};
const proxy = new Proxy(target, handler);
proxy.getDate() // 1
```
## 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`
```javascript
// 老写法
try {
Object.defineProperty(target, property, attributes);
// success
} catch (e) {
// failure
}
// 新写法
if (Reflect.defineProperty(target, property, attributes)) {
// success
} else {
// failure
}
```
3`Object`操作都变成函数行为。某些`Object`操作是命令式,比如`name in obj``delete obj[name]`,而`Reflect.has(obj, name)``Reflect.deleteProperty(obj, name)`让它们变成了函数行为。
```javascript
// 老写法
'assign' in Object // true
// 新写法
Reflect.has(Object, 'assign') // true
```
4`Reflect`对象的方法与`Proxy`对象的方法一一对应,只要是`Proxy`对象的方法,就能在`Reflect`对象上找到对应的方法。这就让`Proxy`对象可以方便地调用对应的`Reflect`方法,完成默认行为,作为修改行为的基础。也就是说,不管`Proxy`怎么修改默认行为,你总可以在`Reflect`上获取默认行为。
```javascript
Proxy(target, {
set: function(target, name, value, receiver) {
var success = Reflect.set(target,name, value, receiver);
if (success) {
log('property ' + name + ' on ' + target + ' set to ' + value);
}
return success;
}
});
```
上面代码中,`Proxy`方法拦截`target`对象的属性赋值行为。它采用`Reflect.set`方法将值赋值给对象的属性,确保完成原有的行为,然后再部署额外的功能。
下面是另一个例子。
```javascript
var loggedObj = new Proxy(obj, {
get(target, name) {
console.log('get', target, name);
return Reflect.get(target, name);
},
deleteProperty(target, name) {
console.log('delete' + name);
return Reflect.deleteProperty(target, name);
},
has(target, name) {
console.log('has' + name);
return Reflect.has(target, name);
}
});
```
上面代码中,每一个`Proxy`对象的拦截操作(`get``delete``has`),内部都调用对应的`Reflect`方法,保证原生行为能够正常执行。添加的工作,就是将每一个操作输出一行日志。
有了`Reflect`对象以后,很多操作会更易读。
```javascript
// 老写法
Function.prototype.apply.call(Math.floor, undefined, [1.75]) // 1
// 新写法
Reflect.apply(Math.floor, undefined, [1.75]) // 1
```
## 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)
上面这些方法的作用,大部分与`Object`对象的同名方法的作用都是相同的,而且它与`Proxy`对象的方法是一一对应的。下面是对它们的解释。
**1Reflect.get(target, name, receiver)**
`Reflect.get`方法查找并返回`target`对象的`name`属性,如果没有该属性,则返回`undefined`
```javascript
var myObject = {
foo: 1,
bar: 2,
get baz() {
return this.foo + this.bar;
},
}
Reflect.get(myObject, 'foo') // 1
Reflect.get(myObject, 'bar') // 2
Reflect.get(myObject, 'baz') // 3
```
如果`name`属性部署了读取函数getter则读取函数的`this`绑定`receiver`
```javascript
var myObject = {
foo: 1,
bar: 2,
get baz() {
return this.foo + this.bar;
},
};
var myReceiverObject = {
foo: 4,
bar: 4,
};
Reflect.get(myObject, 'baz', myReceiverObject) // 8
```
如果第一个参数不是对象,`Reflect.get`方法会报错。
```javascript
Reflect.get(1, 'foo') // 报错
Reflect.get(false, 'foo') // 报错
```
**2Reflect.set(target, name, value, receiver)**
`Reflect.set`方法设置`target`对象的`name`属性等于`value`
```javascript
var myObject = {
foo: 1,
set bar(value) {
return this.foo = value;
},
}
myObject.foo // 1
Reflect.set(myObject, 'foo', 2);
myObject.foo // 2
Reflect.set(myObject, 'bar', 3)
myObject.foo // 3
```
如果`name`属性设置了赋值函数,则赋值函数的`this`绑定`receiver`
```javascript
var myObject = {
foo: 4,
set bar(value) {
return this.foo = value;
},
};
var myReceiverObject = {
foo: 0,
};
Reflect.set(myObject, 'bar', 1, myReceiverObject);
myObject.foo // 4
myReceiverObject.foo // 1
```
如果第一个参数不是对象,`Reflect.set`会报错。
```javascript
Reflect.set(1, 'foo', {}) // 报错
Reflect.set(false, 'foo', {}) // 报错
```
**3Reflect.has(obj, name)**
`Reflect.has`方法对应`name in obj`里面的`in`运算符。
```javascript
var myObject = {
foo: 1,
};
// 旧写法
'foo' in myObject // true
// 新写法
Reflect.has(myObject, 'foo') // true
```
如果第一个参数不是对象,`Reflect.has``in`运算符都会报错。
**4Reflect.deleteProperty(obj, name)**
`Reflect.deleteProperty`方法等同于`delete obj[name]`,用于删除对象的属性。
```javascript
const myObj = { foo: 'bar' };
// 旧写法
delete myObj.foo;
// 新写法
Reflect.deleteProperty(myObj, 'foo');
```
**5Reflect.construct(target, args)**
`Reflect.construct`方法等同于`new target(...args)`,这提供了一种不使用`new`,来调用构造函数的方法。
```javascript
function Greeting(name) {
this.name = name;
}
// new 的写法
const instance = new Greeting('张三');
// Reflect.construct 的写法
const instance = Reflect.construct(Greeting, '张三');
```
**6Reflect.getPrototypeOf(obj)**
`Reflect.getPrototypeOf`方法用于读取对象的`__proto__`属性,对应`Object.getPrototypeOf(obj)`
```javascript
const myObj = new FancyThing();
// 旧写法
Object.getPrototypeOf(myObj) === FancyThing.prototype;
// 新写法
Reflect.getPrototypeOf(myObj) === FancyThing.prototype;
```
`Reflect.getPrototypeOf``Object.getPrototypeOf`的一个区别是,如果第一个参数不是对象(包括`null``undefined``Object.getPrototypeOf`会将这个参数转为对象,然后再运行,而`Reflect.getPrototypeOf`会报错。
```javascript
Object.getPrototypeOf(1) // undefined
Reflect.getPrototypeOf(1) // 报错
```
**7Reflect.setPrototypeOf(obj, newProto)**
`Reflect.setPrototypeOf`方法用于设置对象的`__proto__`属性,对应`Object.setPrototypeOf(obj, newProto)`
```javascript
const myObj = new FancyThing();
// 旧写法
Object.setPrototypeOf(myObj, OtherThing.prototype);
// 新写法
Reflect.setPrototypeOf(myObj, OtherThing.prototype);
```
如果第一个参数不是对象,`Reflect.setPrototypeOf``Object.setPrototypeOf`都会报错。
```javascript
Object.setPrototypeOf(1) // 报错
Reflect.setPrototypeOf(1) // 报错
```
**8Reflect.apply(func, thisArg, args)**
`Reflect.apply`方法等同于`Function.prototype.apply.call(func, thisArg, args)`,用于绑定`this`对象后执行给定函数。
一般来说,如果要绑定一个函数的`this`对象,可以这样写`fn.apply(obj, args)`,但是如果函数定义了自己的`apply`方法,就只能写成`Function.prototype.apply.call(fn, obj, args)`,采用`Reflect`对象可以简化这种操作。
```javascript
const ages = [11, 33, 12, 54, 18, 96];
// 旧写法
const youngest = Math.min.apply(Math, ages);
const oldest = Math.max.apply(Math, ages);
const type = Object.prototype.toString.call(youngest);
// 新写法
const youngest = Reflect.apply(Math.min, Math, ages);
const oldest = Reflect.apply(Math.max, Math, ages);
const type = Reflect.apply(Object.prototype.toString, youngest);
```
**9Reflect.defineProperty(target, propertyKey, attributes)**
`Reflect.defineProperty`方法基本等同于`Object.defineProperty`,用来为对象定义属性。未来,后者会被逐渐废除,请从现在开始就使用`Reflect.defineProperty`代替它。
```javascript
function MyDate() {
/*…*/
}
// 旧写法
Object.defineProperty(MyDate, 'now', {
value: () => new Date.now()
});
// 新写法
Reflect.defineProperty(MyDate, 'now', {
value: () => new Date.now()
});
```
如果`Reflect.defineProperty`的第一个参数不是对象,就会抛出错误,比如`Reflect.defineProperty(1, 'foo')`
**10Reflect.getOwnPropertyDescriptor(target, propertyKey)**
`Reflect.getOwnPropertyDescriptor`基本等同于`Object.getOwnPropertyDescriptor`,用于得到指定属性的描述对象,将来会替代掉后者。
```javascript
var myObject = {};
Object.defineProperty(myObject, 'hidden', {
value: true,
enumerable: false,
});
// 旧写法
var theDescriptor = Object.getOwnPropertyDescriptor(myObject, 'hidden');
// 新写法
var theDescriptor = Reflect.getOwnPropertyDescriptor(myObject, 'hidden');
```
`Reflect.getOwnPropertyDescriptor``Object.getOwnPropertyDescriptor`的一个区别是,如果第一个参数不是对象,`Object.getOwnPropertyDescriptor(1, 'foo')`不报错,返回`undefined`,而`Reflect.getOwnPropertyDescriptor(1, 'foo')`会抛出错误,表示参数非法。
**11Reflect.isExtensible (target)**
`Reflect.isExtensible`方法对应`Object.isExtensible`,返回一个布尔值,表示当前对象是否可扩展。
```javascript
const myObject = {};
// 旧写法
Object.isExtensible(myObject) // true
// 新写法
Reflect.isExtensible(myObject) // true
```
如果参数不是对象,`Object.isExtensible`会返回`false`,因为非对象本来就是不可扩展的,而`Reflect.isExtensible`会报错。
```javascript
Object.isExtensible(1) // false
Reflect.isExtensible(1) // 报错
```
**12Reflect.preventExtensions(target)**
`Reflect.preventExtensions`对应`Object.preventExtensions`方法,用于让一个对象变为不可扩展。它返回一个布尔值,表示是否操作成功。
```javascript
var myObject = {};
// 旧写法
Object.isExtensible(myObject) // true
// 新写法
Reflect.preventExtensions(myObject) // true
```
如果参数不是对象,`Object.isExtensible`在 ES5 环境报错,在 ES6 环境返回这个参数,而`Reflect.preventExtensions`会报错。
```javascript
// ES5
Object.preventExtensions(1) // 报错
// ES6
Object.preventExtensions(1) // 1
// 新写法
Reflect.preventExtensions(1) // 报错
```
**13Reflect.ownKeys (target)**
`Reflect.ownKeys`方法用于返回对象的所有属性,基本等同于`Object.getOwnPropertyNames``Object.getOwnPropertySymbols`之和。
```javascript
var myObject = {
foo: 1,
bar: 2,
[Symbol.for('baz')]: 3,
[Symbol.for('bing')]: 4,
};
// 旧写法
Object.getOwnPropertyNames(myObject)
// ['foo', 'bar']
Object.getOwnPropertySymbols(myObject)
//[Symbol.for('baz'), Symbol.for('bing')]
// 新写法
Reflect.ownKeys(myObject)
// ['foo', 'bar', Symbol.for('baz'), Symbol.for('bing')]
```
## 实例:使用 Proxy 实现观察者模式
观察者模式Observer mode指的是函数自动观察数据对象一旦对象有变化函数就会自动执行。
```javascript
const person = observable({
name: '张三',
age: 20
});
function print() {
console.log(`${person.name}, ${person.age}`)
}
observe(print);
person.name = '李四';
// 输出
// 李四, 20
```
上面代码中,数据对象`person`是观察目标,函数`print`是观察者。一旦数据对象发生变化,`print`就会自动执行。
下面,使用 Proxy 写一个观察者模式的最简单实现,即实现`observable``observe`这两个函数。思路是`observable`函数返回一个原始对象的 Proxy 代理,拦截赋值操作,触发充当观察者的各个函数。
```javascript
const queuedObservers = new Set();
const observe = fn => queuedObservers.add(fn);
const observable = obj => new Proxy(obj, {set});
function set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver);
queuedObservers.forEach(observer => observer());
return result;
}
```
上面代码中,先定义了一个`Set`集合,所有观察者函数都放进这个集合。然后,`observable`函数返回原始对象的代理,拦截赋值操作。拦截函数`set`之中,会自动执行所有观察者。