前言
/ES5和ES6致力与为开发者提供js已有却不可调用的功能。例如在ES5出现以前,JS环境的对象包含许多不可枚举和不可写的属性,但是 开发者不能定义自己的不可枚举或不可写的属性,
于是ES5引入了Object.defineProperty()方法来支持开发者去做引擎早就可以实现的事情,ES6添加了一些内建对象,赋予开发者更多访问JS引擎的能力。
代理(proxy)是一种可以拦截并改变底层JS引擎操作的包装器,在新语言中通过它暴漏的内部运作对象,从而让开发者可以创建内建的对象。
引入
【数组问题】
在ES6之前, 开发者不能通过自己定义对象模仿数组对象的行为。当给数组的特定元素赋值时,影响到该数组的length属性,也可以通过length修改数组元素
let colors = ["red", "green", "blue"];
console.log(colors.length); // 3
colors[3] = "black";
console.log(colors.length); // 4
console.log(colors[3]); // "black"
colors.length = 2;
console.log(colors.length); // 2
console.log(colors[3]); // undefined
console.log(colors[2]); // undefined
console.log(colors[1]); // "green"
colors数组一开始有3个元素,将colors[3]赋值为"black"时,length属性会自动增加到4,
将length属性设置为2时,会移除数组的后两个元素而只保留前两个。在ES5之前开发者无法自己实现这些行为,现在通过代理可以实现。
下表总结了代理陷阱的特性。
每个陷阱覆写JS对象的一些内建特性,可以用他们拦截并修改这些特性,如果仍然需要使用相应的内建特性,则可以使用相应的反射API方法。
创建 简单代理
用Proxy构造函数创建代理需要传入两个参数:目标(target)和处理程序(handle). 处理程序用于定义一个或者多个陷阱函数
带代理中,除了专门操作定义的陷井外,其余操作均使用默认特性,不适用任何陷阱的处理程序等价于简单的代理转发
let target = {};
let proxy = new Proxy(target, {});
proxy.name = "proxy";
console.log(proxy.name); // "proxy"
console.log(target.name); // "proxy"
target.name = "target";
console.log(proxy.name); // "target"
console.log(target.name); // "target"
这个示例中代理将所有的操作都转发到目标,将“proxy”赋值给proxy.name属性上时会在目标上创建name,代理只是简单的将操作转发给目标,
它不会存这个属性,由于proxy.name和target.name引用的都是target.name。 因此二者的值相同,从而为target.name设置新值后,proxy.name也一同变化。
代理陷阱
【使用set陷阱验证属性】
假设创建一个属性值是数字的对象,对象中没增加一个属性都要加以验证,如果不是数字必须抛出错误,为了实现这个任务,可以定义个set陷阱来覆写设置值的默认特性。
set陷阱接受4个参数
trapTarget 用于接受属性(代理的目标)的对象
key 要 写入的属性键(String or Symbol)
value 被写入属性的值
receiver 操作发生的对象(通常是代理) (可选)
let target = {
name: "target"
};
let proxy = new Proxy(target, {
set(trapTarget, key, value, receiver) {
// 忽略已有属性,避免影响它们
if (!trapTarget.hasOwnProperty(key)) {
if (isNaN(value)) {
throw new TypeError("Property must be a number.");
}
}
// 添加属性
return Reflect.set(trapTarget, key, value, receiver);
}
});
// 添加一个新属性
proxy.count = 1;
console.log(proxy.count); // 1
console.log(target.count); // 1
// 你可以为 name 赋一个非数值类型的值,因为该属性已经存在
proxy.name = "proxy";
console.log(proxy.name); // "proxy"
console.log(target.name); // "proxy"
// 抛出错误
proxy.anotherName = "proxy";
set 代理陷阱可以拦截写入的属性。get陷阱可以拦截读取的属性操作
犹豫get陷阱不写入值 所以它复制了set陷阱中除value的其他3个参数, Reflect.get()也接受3个参数并返回属性默认的值,如果属性在目标上不存在,则
使用get陷阱和Reflect.get()时会抛出错误
let proxy = new Proxy({}, {
get (trapTarget, key, receiver) {
if (!(key in receiver)) {
throw new TypeError(key + " does not exist");
}
return Reflect.get(trapTarget, key, receiver);
}
});
proxy.name = "arthur";
proxy.nickname // nickname does not exist
此例中的get陷阱可以拦截属性的读取,并通过in操作符来判断recevier上是否具有被读取的属性,这里之所以用in操作符检查receiver而不是检查trapTarget,
是为了防止receiver代理含有has陷阱
从而得到错误的结果。属性如果不存在就会抛出一个错误,否则就使用默认行为。
使用has陷阱影藏已有属性
可用in操作符来检测给定的对象是否具有某个属性,如果自有属性或者圆形属性匹配到这个名称或者Symbol返回true;
let target = {
value: 42
}
console.log("value" in target) // true
console.log("toString" in target) // true
// value是自有谁能够 toString 是继承Object的属性,二者再对象上都存在,用in 检测都返回
// 在代理中可以使用has陷阱可以拦截这些in操作并返回一个不同的值
// 每当使用in 操作符时都会调用has陷阱,并传入2个参数
// trapTarget 读取属性的对象
// key 要检查的属性
let target = {
name: "target",
value: 42
};
let proxy = new Proxy(target, {
has(trapTarget, key) {
if (key === "value") {
return false;
} else {
return Reflect.has(trapTarget, key);
}
}
});
console.log("value" in proxy); // false
console.log("name" in proxy); // true
console.log("toString" in proxy); // true