代理可以提供拦截目标对象并向基本操作中嵌入额外行为的能力.目标对象可以直接被操作,也可以通过代理来操作
代理是使用 Proxy 构造函数创建的。这个构造函数接收两个参数:目标对象和处理程序对象.
const target = {
id: "target",
};
const handler = {};
const proxy = new Proxy(target, handler);
// 访问id会访问同一个值
console.log(target.id); //target
console.log(proxy.id); //target
// 给目标属性赋值会反应在目标对象和代理对象上
target.id = "foo";
console.log(target.id); //foo
console.log(proxy.id); //foo
// 给代理对象赋值也会反映在两个对象上
proxy.id = "bar";
console.log(target.id); //bar
console.log(proxy.id); //bar
// hasOwnProperty()方法在两个地方都会应用到目标对象上
console.log(target.hasOwnProperty("id")); //true
console.log(proxy.hasOwnProperty("id")); //true
// Proxy的原型对象是undefined
// 因此不能使用instanceof操作符
console.log(Proxy.prototype); //undefined
console.log(target instanceof Proxy); //TypeError: Function has non-object prototype 'undefined' in instanceof check
console.log(proxy instanceof Proxy); //TypeError: Function has non-object prototype 'undefined' in instanceof check
// 严格相等可以区分代理和目标对象
console.log(proxy === target); //false
使用代理的主要目的是可以定义捕获器(trap)。捕获器就是在处理程序对象中定义的“基本操作的
拦截器”.每次在代理对象上调用这些基本操作时,代理可以在这些操作传播到目标对
象之前先调用捕获器函数,从而拦截并修改相应的行为。
const target = {
foo: "bar",
};
const handler = {
// get捕获器,当代理对象执行get操作时,即访问通过代理对象访问目标对象属性时会触发该捕获器
get() {
return "handler override";
},
};
const proxy = new Proxy(target, handler);
console.log(target.foo); //bar
console.log(proxy.foo); //handler override
console.log(target["foo"]); //bar
console.log(proxy["foo"]); //handler override
console.log(Object.create(target)["foo"]); //bar
console.log(Object.create(proxy)["foo"]); //handler override
所有捕获器都可以访问相应的参数,基于这些参数可以重建被捕获方法的原始行为。
const target = {
foo: "bar",
};
const handler = {
// 参数依次是 目标对象,要访问的属性名,代理对象
get(trapTarget, property, receiver) {
console.log(trapTarget === target);
console.log(property);
console.log(receiver === proxy);
},
};
const proxy = new Proxy(target, handler);
proxy.foo;
// 访问代理对象proxy.foo,触发get拦截器,依次打印
// true
// foo
// true
// 有了这些参数就可以重建原始操作
事实上,开发者不需要手动重建原始行为,可通过全局的 Reflect 对象(封装了原始行为)的同名方法来重建,处理程序对象中所有可以捕获的方法都有对应的反射(Reflect)API 方法。这些方法与捕获器拦截的方法具有相同的名称和函数签名,而且也具有与被拦截方法相同的行为.因此可以定义没有任何额外行为的空代理.
const target = {
foo: "bar",
};
const handler = {
get: Reflect.get,
};
const proxy = new Proxy(target, handler);
console.log(target.foo); //bar
console.log(proxy.foo); //bar
// 创建捕获所有方法的代理,直接使用Reflect作为处理程序对象
const proxy2 = new Proxy(target, Reflect);
console.log(target.foo); //bar
console.log(proxy2.foo); //bar
捕获处理程序的行为必须遵循“捕获器不变式”。捕获器不变式因方法不同而异,但通常都会防止捕获器定义出现过于反常的行为。如目标对象有一个不可配置且不可写的数据属性,那么在捕获器返回一个与该属性不同的值时,会抛出 TypeError.
const target = {};
// foo属性不可写且不可配置
Object.defineProperty(target, "foo", {
configurable: false,
writable: false,
value: "bar",
});
// get捕获器返回不是原来的value,那么访问proxy.foo会报错
const handler = {
get() {
return "qux";
},
};
const proxy = new Proxy(target, handler);
proxy.foo; //TypeError
中断代理对象与目标对象之间的联系.Proxy 也暴露了 revocable()方法,这个方法支持撤销代理对象与目标对象的关联。
const target = {
foo: "bar",
};
const handler = {
get() {
return "intercepted";
},
};
const { proxy, revoke } = Proxy.revocable(target, handler);
console.log(proxy.foo); //intercepted
console.log(target.foo); // bar
revoke(); // 调用revoke函数
console.log(proxy.foo); //TypeError: Cannot perform 'get' on a proxy that has been revoked
某些情况下应该优先使用反射 API,这是有一些理由的
注意
很多反射方法返回称作“状态标记”的布尔值,表示意图执行的操作是否成功.
以下反射方法都会返回状态标记:
const o = {};
if (Reflect.defineProperty(o, "foo", { value: "bar" })) {
console.log("success");
} else {
console.log("failure");
}
以下反射方法提供只有通过操作符才能完成的操作:
创建一个代理另一个代理的代理
const target = {
foo: "bar",
};
const firstProxy = new Proxy(target, {
get() {
console.log("第一个代理");
return Reflect.get(...arguments);
},
});
const secondProxy = new Proxy(firstProxy, {
get() {
console.log("第二个代理");
return Reflect.get(...arguments);
},
});
console.log(secondProxy.foo);
// 第二个代理
// 第一个代理
// bar
如果目标对象某些方法依赖对象实例,那么代理该目标对象会出错,要代理其构造函数可以解决
代理可以捕获 13 种不同的基本操作,有各自不同的反射 API 方法,参数,关联 ECMAScript 操作和不变式,在代理上执行的任何一种操作只会有一个捕获处理程序被调用,不存在重复捕获的情况
get()捕获器在获取属性值的操作被调用,对应反射 API-Reflect.get()
const myTarget = {};
const proxy = new Proxy(myTarget, {
/**
*
* @param {*} target 目标对象
* @param {*} property 引用的目标对象上的字符串键属性
* @param {*} receiver 代理对象或继承代理对象的对象
* @returns
*/
get(target, property, receiver) {
console.log("get()");
return Reflect.get(...arguments);
},
});
// 拦截的操作
proxy.foo; // get()
proxy["foo"]; // get()
Object.create(proxy)["foo"]; // get()
Reflect.get(proxy, "foo"); // get()
// 返回值无限制
// 捕获不变式
// 如果target.property 不可写且不可配置,则处理程序返回的值必须与target.property 匹配。
// 如果target.property 不可配置且[[Get]]特性为undefined,处理程序的返回值也必须是undefined。
set()捕获器会在设置属性值的操作中被调用。对应的反射 API 方法为 Reflect.set()。
const myTarget = {};
const proxy = new Proxy(myTarget, {
/**
*
* @param {*} target 目标对象
* @param {*} property 引用的目标对象上的字符串键属性
* @param {*} value 要赋给属性的值
* @param {*} receiver 代理对象或继承代理对象的对象
* @returns // 返回true表示成功,false表示失败
*/
set(target, property, value, receiver) {
console.log("set()");
return Reflect.set(...arguments);
},
});
// 拦截的操作
proxy.foo = "bar"; // set()
proxy["foo"] = "bar"; // set()
Object.create(proxy)["foo"] = "bar"; // set()
Reflect.set(proxy, "foo", "bar"); // set()
// 捕获不变式
// 如果target.property 不可写且不可配置,则不能修改目标属性的值。
// 如果target.property 不可配置且[[Set]]特性为undefined,则不能修改目标属性的值。
// 在严格模式下,处理程序中返回false 会抛出TypeError。
has()捕获器会在 in 操作符中被调用。对应的反射 API 方法为 Reflect.has()。
const myTarget = {};
const proxy = new Proxy(myTarget, {
/**
*
* @param {*} target 目标对象
* @param {*} property 引用的目标对象上的字符串键属性
* @returns has()必须返回布尔值,表示属性是否存在
*/
has(target, property) {
console.log("has()");
return Reflect.has(...arguments);
},
});
// 拦截的操作
"foo" in proxy; //has()
"foo" in Object.create(proxy); //has()
Reflect.has(proxy, "foo"); //has()
// 捕获不变式
// 如果target.property 存在且不可配置,则处理程序必须返回true。
// 如果target.property 存在且目标对象不可扩展,则处理程序必须返回true。
defineProperty()捕获器会在 Object.defineProperty()中被调> 用。对应的反射 API 方法为
Reflect.defineProperty()。
const myTarget = {};
const proxy = new Proxy(myTarget, {
/**
*
* @param {*} target 目标对象
* @param {*} property 引用的目标对象上的字符串键属性
* @param {*} descriptor 包含可选的enumerable、configurable、writable、value、get 和set
定义的对象
* @returns defineProperty()必须返回布尔值,表示属性是否成功定义
*/
defineProperty(target, property, descriptor) {
console.log("defineProperty()");
return Reflect.defineProperty(...arguments);
},
});
// 拦截的操作
Object.defineProperty(proxy, "foo", { value: "bar" });
Reflect.defineProperty(proxy, "foo", { value: "bar" });
// defineProperty();
// defineProperty();
// 捕获不变式
// 如果目标对象不可扩展,则无法定义属性。
// 如果目标对象有一个可配置的属性,则不能添加同名的不可配置属性。
// 如果目标对象有一个不可配置的属性,则不能添加同名的可配置属性。
getOwnPropertyDescriptor()捕获器会在 Object.getOwnPropertyDescriptor()中被调
用。对应的反射 API 方法为 Reflect.getOwnPropertyDescriptor()。
const myTarget = {};
const proxy = new Proxy(myTarget, {
/**
*
* @param {*} target 目标对象
* @param {*} property 引用的目标对象上的字符串键属性
* @returns getOwnPropertyDescriptor()必须返回对象,或者在属性不存在时返回undefined
*/
getOwnPropertyDescriptor(target, property) {
console.log("getOwnPropertyDescriptor()");
return Reflect.getOwnPropertyDescriptor(...arguments);
},
});
// 拦截的操作
Object.getOwnPropertyDescriptor(proxy, "foo");
Reflect.getOwnPropertyDescriptor(proxy, "foo");
// getOwnPropertyDescriptor();
// getOwnPropertyDescriptor();
// 捕获不变式
// 如果自有的target.property 存在且不可配置,则处理程序必须返回一个表示该属性存在的
// 对象。
// 如果自有的target.property 存在且可配置,则处理程序必须返回表示该属性可配置的对象。
// 如果自有的target.property 存在且target 不可扩展,则处理程序必须返回一个表示该属性存
// 在的对象。
// 如果target.property 不存在且target 不可扩展,则处理程序必须返回undefined 表示该属
// 性不存在。
// 如果target.property 不存在,则处理程序不能返回表示该属性可配置的对象。
deleteProperty()捕获器会在 delete 操作符中被调用。对应的反射 API 方法为 Reflect.
deleteProperty()。
const myTarget = {};
const proxy = new Proxy(myTarget, {
/**
*
* @param {*} target 目标对象
* @param {*} property 引用的目标对象上的字符串键属性
* @returns deleteProperty()必须返回布尔值,表示删除属性是否成功
*/
deleteProperty(target, property) {
console.log("deleteProperty()");
return Reflect.deleteProperty(...arguments);
},
});
// 拦截的操作
delete proxy.foo;
delete proxy["foo"];
Reflect.deleteProperty(proxy, "foo");
// deleteProperty();
// deleteProperty();
// deleteProperty();
// 捕获不变式
// 如果自有的target.property 存在且不可配置,则处理程序不能删除这个属性。
ownKeys()捕获器会在 Object.keys()及类似方法中被调用。对应的反射 API 方法为 Reflect.
ownKeys()。
const myTarget = {};
const proxy = new Proxy(myTarget, {
/**
*
* @param {*} target 目标对象
* @returns ownKeys()必须返回包含字符串或符号的可枚举对象。
*/
ownKeys(target) {
console.log("ownKeys()");
return Reflect.ownKeys(...arguments);
},
});
// 拦截的操作
Object.getOwnPropertyNames(proxy);
Object.getOwnPropertySymbols(proxy);
Object.keys(proxy);
Reflect.ownKeys(proxy);
// ownKeys();
// ownKeys();
// ownKeys();
// ownKeys();
// 捕获不变式
// 返回的可枚举对象必须包含target 的所有不可配置的自有属性。
// 如果target 不可扩展,则返回可枚举对象必须准确地包含自有属性键。
getPrototypeOf()捕获器会在 Object.getPrototypeOf()中被调用。对应的反射 API 方法为
Reflect.getPrototypeOf()。
const myTarget = {};
const proxy = new Proxy(myTarget, {
/**
*
* @param {*} target 目标对象
* @returns getPrototypeOf()必须返回对象或null。
*/
getPrototypeOf(target) {
console.log("getPrototypeOf()");
return Reflect.getPrototypeOf(...arguments);
},
});
// 拦截的操作
Object.getPrototypeOf(proxy);
Reflect.getPrototypeOf(proxy);
proxy.__proto__;
Object.prototype.isPrototypeOf(proxy);
proxy instanceof Object;
// getPrototypeOf();
// getPrototypeOf();
// getPrototypeOf();
// getPrototypeOf();
// getPrototypeOf();
// 捕获不变式
// 如果target 不可扩展,则Object.getPrototypeOf(proxy)唯一有效的返回值就是Object.
// getPrototypeOf(target)的返回值。
setPrototypeOf()捕获器会在 Object.setPrototypeOf()中被调用。对应的反射 API 方法为
Reflect.setPrototypeOf()。
const myTarget = {};
const proxy = new Proxy(myTarget, {
/**
*
* @param {*} target 目标对象
* @param {*} prototype target 的替代原型,如果是顶级原型则为null。
* @returns setPrototypeOf()必须返回布尔值,表示原型赋值是否成功
*/
setPrototypeOf(target, prototype) {
console.log("setPrototypeOf()");
return Reflect.setPrototypeOf(...arguments);
},
});
// 拦截的操作
Object.setPrototypeOf(proxy, Object);
Reflect.setPrototypeOf(proxy, Object);
// setPrototypeOf();
// setPrototypeOf();
// 捕获不变式
// 如果target 不可扩展,则唯一有效的prototype 参数就是Object.getPrototypeOf(target)
// 的返回值。
isExtensible()捕获器会在 Object.isExtensible()中被调用。对应的反射 API 方法为
Reflect.isExtensible()。
const myTarget = {};
const proxy = new Proxy(myTarget, {
/**
*
* @param {*} target 目标对象
* @returns isExtensible()必须返回布尔值,表示target 是否可扩展
*/
isExtensible(target) {
console.log("isExtensible()");
return Reflect.isExtensible(...arguments);
},
});
// 拦截的操作
Object.isExtensible(proxy);
Reflect.isExtensible(proxy);
// isExtensible();
// isExtensible();
// 捕获不变式
// 如果target 可扩展,则处理程序必须返回true。
// 如果target 不可扩展,则处理程序必须返回false。
preventExtensions()捕获器会在 Object.preventExtensions()中被调用。对应的反射 API
方法为 Reflect.preventExtensions()。preventExtensions 表示对象不能再添加新的属性,但是可以修改原有属性
const myTarget = {};
const proxy = new Proxy(myTarget, {
/**
*
* @param {*} target 目标对象
* @returns preventExtensions()必须返回布尔值,表示target 是否已经不可扩展
*/
preventExtensions(target) {
console.log("preventExtensions()");
return Reflect.preventExtensions(...arguments);
},
});
// 拦截的操作
Object.preventExtensions(proxy);
Reflect.preventExtensions(proxy);
// preventExtensions();
// preventExtensions();
// 捕获不变式
// 如果Object.isExtensible(proxy)是false,则处理程序必须返回true。
apply()捕获器会在调用函数时中被调用。对应的反射 API 方法为 Reflect.apply()。
const myTarget = () => {};
const proxy = new Proxy(myTarget, {
/**
*
* @param {*} target 目标对象
* @param {*} thisArg 调用函数时的this参数
* @param {*} argumentsList 调用函数时的参数列表
* @returns 返回值无限制
*/
apply(target, thisArg, argumentsList) {
console.log("apply()");
return Reflect.apply(...arguments);
},
});
// 拦截的操作
proxy();
proxy.call(proxy);
proxy.apply(proxy);
Reflect.apply(proxy, proxy, []);
// 捕获不变式
// target 必须是一个函数对象。
construct()捕获器会在 new 操作符中被调用。对应的反射 API 方法为 Reflect.construct()。
const myTarget = function () {};
const proxy = new Proxy(myTarget, {
/**
*
* @param {*} target 目标构造函数
* @param {*} argumentsList 传给目标构造函数的参数列表
* @param {*} newTarget 代理构造函数
* @returns construct()必须返回一个对象。
*/
construct(target, argumentsList, newTarget) {
console.log("construct()");
console.log(newTarget === proxy);
return Reflect.construct(...arguments);
},
});
// 拦截的操作
new proxy();
Reflect.construct(proxy, []);
// construct();
// true;
// construct();
// true;
// 捕获不变式
// target 必须可以用作构造函数。
使用代理可以在代码中实现一些有用的编程模式
通过捕获 get、set 和 has 等操作,可以知道对象属性什么时候被访问、被查询。把实现相应捕获
器的某个对象代理放到应用中,可以监控这个对象何时在何处被访问过
const user = {
name: "Dragon",
};
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 = 28; //setting age = 28
代理的内部实现对外部代码是不可见的,利用 get()和 has()来隐藏某些属性,使得访问该属性时 undefined
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
console.log("baz" in proxy); // true
所有赋值操作都会触发 set()捕获器,所以可以根据所赋的值决定是允许还是拒绝赋值
const target = {
onlyNumberGoHere: 0,
};
const proxy = new Proxy(target, {
set(target, property, value) {
if (typeof value !== "number") {
return false;
} else {
return Reflect.set(...arguments);
}
},
});
proxy.onlyNumberGoHere = 1;
console.log(proxy.onlyNumberGoHere); //1
proxy.onlyNumberGoHere = "2"; //这里设置了"2",不是数字,赋值操作失败
console.log(proxy.onlyNumberGoHere); //1
跟保护和验证对象属性类似,也可对函数和构造函数参数进行审查
// 函数
// 中位数
function median(...nums) {
return nums.sort()[Math.floor(nums.length / 2)];
}
const proxy = new Proxy(median, {
apply(target, thisArg, argArray) {
for (let item of argArray) {
if (typeof item !== "number") {
throw "Non-number argument provided";
}
}
return Reflect.apply(...arguments);
},
});
console.log(proxy(4, 7, 1)); //4
console.log(proxy("4", 7, 1)); // Non-number argument provided
// 构造函数
class User {
constructor(id) {
this.id_ = id;
}
}
const proxy = new Proxy(User, {
construct(target, argArray) {
if (argArray[0] === undefined) {
throw new Error("User cannot be instantiated without id");
} else {
return Reflect.construct(...arguments);
}
},
});
new proxy(1);
new proxy();
//Error: User cannot be instantiated without id
通过代理可以把运行时中原本不相关的部分联系到一起。这样就可以实现各种模式,从而让不同的
代码互操作
//比如,可以将被代理的类绑定到一个全局实例集合,让所有创建的实例都被添加到这个集合中:
const userList = [];
class User {
constructor(name) {
this.name = name;
}
}
const UserProxy = new Proxy(User, {
construct() {
const newUser = Reflect.construct(...arguments);
userList.push(newUser);
return newUser;
},
});
new UserProxy("john");
new UserProxy("jacob");
new UserProxy("mathew");
console.log(userList);
// [
// User { name: 'john' },
// User { name: 'jacob' },
// User { name: 'mathew' }
// ]
//另外,还可以把集合绑定到一个事件分派程序,每次插入新实例时都会发送消息:
const userList = [];
function emit(newValue) {
console.log(newValue);
}
const proxy = new Proxy(userList, {
set(target, property, value, receiver) {
const result = Reflect.set(...arguments);
if (result) {
emit(Reflect.get(target, property, receiver));
}
return result;
},
});
proxy.push("John");
// John
// 1
proxy.push("Jacob");
// Jacob
// 2