ES6 学习补漏(二)-Proxy和Reflect
Proxy
一、Proxy
- MDN 的描述:Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)
- 红宝书的描述:代理是目标对象的抽象。从很多方面看,代理类似 C++指针,因为它可以用作目标对象的替身,但又完全独立于目标对象。目标对象既可以直接被操作,也可以通过代理来操作。但直接操作会绕过代理施予的行为。
- 我自己在看的一个ES6的文档:Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种“元编程”(meta programming),即对编程语言进行编程。
下面是定义的步骤
const target = {
id: 'target',
};
const handler = {
get: function (target, key, receiver) {
console.log(`getting ${key}!`);
return Reflect.get(target, key, receiver);
},
};
const proxy = new Proxy(target, handler);
//可以通过proxy 来访问源对象上面的数据 和 方法
// id 属性会访问同一个值
console.log(target.id); // target
console.log(proxy.id); // getting id! target
console.log(proxy.uuid); // getting uuid! undefined
二、Proxy 的主要目的于用途
- 使用代理的主要目的是可以定义捕获器(trap)。捕获器就是处理程序对象中定义的“基本操作的拦截器”。(红宝书P267 9.1.2)。每次在代理对象上调用这些基本操作时,代理可以在这些操作传播到目标对象之前先调用捕获器函数,从而拦截并修改相应的行为。(上面的修改的GET方法)
- 和Reflect 一起使用,好像在VUE3.0中,就是用Proxy和Reflect一起实现数据的跟新的(可见视频教程)。有一说一,天禹老师讲的很基础,对我这种只会用,不了解原理的人帮助挺大的。
- 跟踪属性访问
通过捕获 get、set 和 has 等操作,可以知道对象属性什么时候被访问、被查询。把实现相应捕获
器的某个对象代理放到应用中,可以监控这个对象何时在何处被访问过
const user = {
name: 'Jake'
};
const proxy = new Proxy(user, {
get(target, property, receiver) {
console.log(`Getting ${property}`);
return Reflect.get(...arguments);
},
set(target, property, value, receiver) {
console.log(`Setting ${property}=${value}`);
return Reflect.set(...arguments);
}
});
proxy.name; // Getting name
proxy.age = 27; // Setting age=27
- 隐藏属性
代理的内部实现对外部代码是不可见的,因此要隐藏目标对象上的属性也轻而易举。比如:
const hiddenProperties = ['foo', 'bar'];
const targetObject = {
foo: 1,
bar: 2,
baz: 3
};
const proxy = new Proxy(targetObject, {
get(target, property) {
if (hiddenProperties.includes(property)) {
return undefined;
} else {
return Reflect.get(...arguments);
}
},
has(target, property) {
if (hiddenProperties.includes(property)) {
return false;
} else {
return Reflect.has(...arguments);
}
}
});
// get()
console.log(proxy.foo); // undefined
console.log(proxy.bar); // undefined
console.log(proxy.baz); // 3
// has()
console.log('foo' in proxy); // false
console.log('bar' in proxy); // false
- 属性验证
因为所有赋值操作都会触发 set()捕获器,所以可以根据所赋的值决定是允许还是拒绝赋值:
const target = {
onlyNumbersGoHere: 0
};
const proxy = new Proxy(target, {
set(target, property, value) {
if (typeof value !== 'number') {
return false;
} else {
return Reflect.set(...arguments);
}
}
});
proxy.onlyNumbersGoHere = 1;
console.log(proxy.onlyNumbersGoHere); // 1
proxy.onlyNumbersGoHere = '2';
console.log(proxy.onlyNumbersGoHere); // 1
- 函数与构造函数参数验证
跟保护和验证对象属性类似,也可对函数和构造函数参数进行审查。比如,可以让函数只接收某种类型的值:
function median(...nums) {
return nums.sort()[Math.floor(nums.length / 2)];
}
const proxy = new Proxy(median, {
apply(target, thisArg, argumentsList) {
for (const arg of argumentsList) {
if (typeof arg !== 'number') {
throw 'Non-number argument provided';
}
}
return Reflect.apply(...arguments);
}
});
console.log(proxy(4, 7, 1)); // 4
console.log(proxy(4, '7', 1));
// Error: Non-number argument provided
- 实现观察者模式
观察者模式(Observer mode)指的是函数自动观察数据对象,一旦对象有变化,函数就会自动执行。
const person = observable({
name: '张三',
age: 20
});
function print() {
console.log(`${person.name}, ${person.age}`)
}
observe(print);
person.name = '李四';// 李四, 20
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;
}
三、取消Proxy 与存在的问题
Proxy.revocable方法返回一个可取消的Proxy实例。大致步骤如下
let target = {};
let handler = {};
let {proxy, revoke} = Proxy.revocable(target, handler);
proxy.foo = 123;
proxy.foo // 123
revoke();
proxy.foo // TypeError: Revoked
- this 问题
代理潜在的一个问题来源是 this 值。我们知道,方法中的 this 通常指向调用这个方法的对象,但是在Proxy代理的时候,this指向关键字是指向Proxy代理的:
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。
- 代理与内部槽位问题(红宝书上中提出)
代理与内置引用类型(比如 Array)的实例通常可以很好地协同,但有些 ECMAScript 内置类型可能会依赖代理无法控制的机制,结果导致在代理上调用某些方法会出错。一个典型的例子就是 Date 类型。根据 ECMAScript 规范,Date 类型方法的执行依赖 this 值上的内部槽位[[NumberDate]]。代理对象上不存在这个内部槽位,而且这个内部槽位的值也不能通过普通的 get()和 set()操作访问到,于是代理拦截后本应转发给目标对象的方法会抛出 TypeError:
const target = new Date();
const proxy = new Proxy(target, {});
console.log(proxy instanceof Date); // true
proxy.getDate(); // TypeError: 'this' is not a Date object
四、提前终止生成器
与迭代器类似,生成器也支持“可关闭”的概念。一个实现 Iterator 接口的对象一定有 next()方法,还有一个可选的 return()方法用于提前终止迭代器。生成器对象除了有这两个方法,还有第三个方法:throw()。
- return()方法会强制生成器进入关闭状态。提供给 return()方法的值,就是终止迭代器对象的值。
- throw()方法会在暂停的时候将一个提供的错误注入到生成器对象中。如果错误未被处理,生成器就会关闭 。
Reflect
一、Reflect
自己的理解:和Proxy很像,但是不能通过new关键字生成。
MDN:Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。这些方法与proxy handlers (en-US)的方法相同。Reflect不是一个函数对象,因此它是不可构造的。
使用例子
const duck = {
name: 'Maurice',
color: 'white',
greeting: function() {
console.log(`Quaaaack! My name is ${this.name}`);
}
}
Reflect.has(duck, 'color');// true
Reflect.has(duck, 'haircut');// false
Reflect.ownKeys(duck);//查询属性
// [ "name", "color", "greeting" ]
Reflect.set(duck, 'eyes', 'black');//添加属性
// returns "true" if successful
// "duck" now contains the property "eyes: 'black'"