Proxy
代理是指对对象(也包含函数)行为进行拦截,设置"陷阱"( trap
),来定义对象的行为,同时也能创建内置对象。
一.Proxy 和 Reflection
代理可视化目标,因此代理和目标对象功能相同。代理允许拦截普通对象操作,这些操作一般都是内部行为,通过 trap
来完成,trap
表示 function。
Reflection API: 通过 Reflect
对象来表示,是方法集合,这些方法和代理能够重写的普通操作的行为一致。每一个proxy trap 都对应着一个 Reflect method,并且他们的签名一致。
1.trap
Each trap overrides some built-in behavior of Javascript objects, allowing you to intercept and modify the behavior。
trap分类:
get(trapTarget, key, receiver) / set(trapTarget, key, value, receiver)
has(trapTarget, key)
deleteProperty(trapTarget, key) / defineProperty(trapTarget, key, descriptor)
getPrototypeof(trapTarget) / setPrototypeOf(trapTarget, proto)
isExtensible(trapTarget) / preventExtensions(trapTarget)
getOwnPropertyDescriptor(traoTarget, key)
ownKeys(trapTarget)
apply(trapTarget, thisArg, argumentsList) / construct(trapTarget, argumentsList)
对应的Reflect方法与之一致。
2.简单的代理构造器
Proxy构造器包含2个参数:一个目标对象,一个handler对象包含trap方法
var target = {};
var proxy = new Proxy(target, {}); // 这个代理不包含trap
target.name = "james";
console.log(proxy.name); // "james"
二.验证属性--set, 对象形状验证--get
1.set
拦截书写写入(write),重写默认的set行为,属性设置成功返回true,反之false
4个参数:
-
trapTarget
: 代理的目标;
-
key
: 属性key(string 或 symbol);
-
value
: 要写入的属性值;
-
receiver
: 实际发生操作的对象,通常是代理本身
Reflect.set()是set trap相对应的反射方法。
比如加入想添加的属性值必须是数字类型,可以通过set来监视(inspect)传入的值:
var target = {name: "james"};
var proxy = new Proxy(target, {
set(trapTarget, key, value, receiver) {
// 忽略存在的值不受影响
if (!trapTarget.hasOwnProperty(key)) {
if (isNaN(value)) {
throw new Error("property must be numeric");
}
}
// 添加属性
return Reflect.set(trapTarget, key, value, receiver);
}
});
// 添加属性
proxy.age = 26;
// 因为target中已经存在"name"属性, 所以可以更改
proxy.name = "kobe"; // OK
// 再添加属性,不是数值, 抛出异常
// Uncaught error: property must be numeric
proxy.job = "software engineer";
// 同时目标对象通过反射,也会添加属性"age"
console.log(target); // {name: "kobe", age: 26}
2.get
拦截对象读取(read)行为
3个参数:
-
trapTarget
: 代理的对象;
-
key
: 属性key;
-
receiver
: 实际发生操作的对象(通常是proxy本身)
javascript读取不存在的属性一般返回undefined,而其他语言读取不存在的属性一般抛出错误。
let target = {name: "james"};
console.log(target.age); // "undefined"
可以通过 get
trap 和 Reflect.get()
改变这种情况:
let proxy = new Proxy({}, {
get(trapTarget, key, receiver) {
// 此处使用receiver 而不是trapTarget,这是考虑到 has trap的原因,下面会讲到
if (!(key in receiver)) {
throw new TypeError(`property ${key} doesn't exist`);
}
return Reflect.get(trapTarget, key, receiver);
}
});
proxy.name = "james";
// 读取
console.log(proxy.name); // "james" ok
// 不小心打错了name属性
console.log(proxy.nme); // throw error: property nme doesn't exist
三.隐藏属性的存在---has
拦截对象存在性
in
操作符对存在在对象自身或原型上的属性,返回true, 可以通过 has trap
拦截这种操作,当使用 "in" 操作符时, has trap将会被调用。
2个参数:
-
trapTarget
: 代理的目标对象;
-
key
: 用于检验的键
可以使用 has
和Reflect.has()
来拦截这种操作
let target = {
name: "james",
age: 26
};
let proxy = new Proxy(target, {
has(trapTarget, key) {
if ("name" in trapTarget) {
return false;
} else {
return Reflect.has(trapTarget, key);
}
}
});
console.log("name" in target); // false
console.log("toString" in target); // true
这也可以解释为什么上面的get trap中要使用 receiver, 而不是trapTarget来作为逻辑
四.阻止删除行为---deleteProperty
拦截删除行为
2个参数:
-
trapTarget
: 代理的目标对象;
-
key
: 拦截删除的键
delete
操作符用于删除对象属性,如果删除成功返回true,失败false,对于 nonconfigurable 的属性, 在非严格模式下,返回false,对于严格模式则抛出异常。
let target = {
name: "james",
age: 26
};
let proxy = new Proxy(target, {
deleteProperty(trapTarget, key) {
// 禁止删除name属性
if (key === "name") {
return false;
} else {
return Reflect.deleteProperty(trapTarget, key);
}
}
});
delete proxy.name; // false
console.log("name" in proxy); // true
// 当然这些拦截行为都是针对代理对象而言的
// 直接删除目标对象属性是可以得,不过这样就失去了代理的意义了
delete target.name; // true
五.对象原型代理--getPrototypeOf / setPrototypeOf
代理允许拦截 Object.getPrototypeOf()
和 Object.setPrototypeOf()
两个方法的执行。
原型代理的限制:
-
getPrototypeOf trap
必须返回一个对象或者null,其余的返回值都不出现runtime error;
-
setPrototypeOf trap
操作失败必须返回false, 这样 Object.setPrototypeOf()会抛出异常.如果返回其余值则Object.setPrototypeOf()操作成功
参数:
-
trapTarget
: 代理的目标对象;getPrototypeOf trap
只有这一个参数
-
proto
: 原型对象
let target = {};
let proxy = new Proxy(target, {
getPrototypeOf(trapTarget) {
return null;
},
setPrototypeOf(trapTarget, proto) {
return false;
}
});
// 再次强调这些操作是针对代理自身的, 目标对象是不受影响的
var targetProto = Object.getPrototypeOf(target);
console.log(targetProto === Object.prototype); // true
Object.setPrototypeOf(target, {}); // 成功
var proxyProto = Object.getPrototypeOf(proxy); // 返回null
console.log(proxyProto === Object.prototype); // false
console.log(proxyProto); // null
Object.setPrototypeOf(proxy); // 抛出错误
如果要使用默认的行为,则使用相应的Reflect方法
let target = {};
let proxy = new Proxy(target, {
getPrototypeOf(trapTarget) {
return Reflect.getPrototypeOf(trapTarget);
},
setPrototypeOf(trapTarget, proto) {
return Reflect.setPrototypeOf(trapTarget, proto);
}
});
// 这样target 和 proxy 都可以使用2个方法了
六.对象扩展性--isExtensible / preventExtensions
是否阻止对象的可扩展性
1个参数:
trapTarget
: 代理的目标对象
let target = {};
let proxy = new Proxy(target, {
isExtensible(trapTarget) {
return Reflect.isExtensible(trapTarget);
},
// 使Object.preventExtensions()失效
preventExtensions(trapTarget) {
return false;
}
});
console.log(Object.isExtensible(target)); // true
console.log(Object.isExtensible(proxy)); // true
Object.preventExtensions(proxy); // 无效
console.log(Object.isExtensible(target)); // true
console.log(Object.isExtensible(proxy)); // true
七.属性描述器---getOwnPropertyDescriptor
拦截调用Object.defineProperty() 和 Object.getOwnPropertyDescriptor()方法, 通过相应的拦截器
1.defineProperty
3个参数:
-
trapTarget
: 代理的目标对象;
-
key
: string 或者 symbol 属性;
-
descriptor
: 对象属性的描述器
defineProperty拦截器如果操作成功返回true, 失败返回false
屏蔽属性定义
let proxy = new Proxy({}, {
defineProperty(trapTraget, key, descriptor) {
// 拦截symbol类型的属性
if (typeof key === "symbol") {
return false;
}
return Reflect.defineProperty(trapTarget, key, descriptor);
}
});
Object.defineProperty(proxy, "name", {
value: "james";
}); // 成功
var myId = Symbol("unique ID");
Object.defineProperty(proxy, myId, {
value: 1234;
}); // 抛出错误
2.getOwnPropertyDescriptor
返回值为null, undefined或者一个对象
2个参数:
-
trapTarget
: 代理的目标对象;
-
key
: string 或者 symbol 属性;
对应的Reflect方法接受相同的参数, 描述器对应的属性只能有enumerable, wriable, configurable, get, set, value
let proxy = new Proxy({}, {
defineProperty(trapTarget, key, descriptor) {
console.log(trapTarget.value); // "value" 是允许的描述器
console.log(trapTarget.name); // "name" 不被允许
return Reflect.defineProperty(trapTarget, key, descriptor);
}
});
Object.defineProperty(proxy, "name", {
value: "proxy",
name: "custom"
});
// 输出
// "proxy"
// "custom"
八.ownKeys
用于拦截内部属性[[OwnPropertyKeys]], 允许重写一个数组返回值内容,数组包括Object.keys(), Object.getOwnPropertyNames(), Object.getOwnPropertySymbols()和Object.assign();
1个参数:
-
trapTarget
: 代理的目标对象;
必须返回一个数组或者类数组对象
let proxy = new Proxy({}, {
ownKeys(trapTarget) {
return Reflect.ownKeys(trapTarget).filter(key => {
return typeof key !== "string" || key[0] !== "_";
});
}
});
let nameSymbol = Symbol("name");
proxy.name = "james";
proxy._name = "private";
proxy[nameSymbol] = "symbol";
let keys = Object.keys(proxy);
console.log(keys.length); // 1
console.log(keys[0]); // "james"
let names = Object.getOwnPropertyNames(proxy);
console.log(names.length); // 1
console.log(names[0]); // "james"
let symbols = Object.getOwnPropertySymbols(proxy);
console.log(symbols.length); // 1
console.log(symbols[0]); // "Symbol(name)"
九.函数的调用方式---apply / construct
拦截函数的调用方式
1.apply
3个参数:
-
trapTarget
: 代理的目标对象;
-
thisArg
: 函数调用时this的值
-
argumentsList
: 传入的参数数组
2.construct
2个参数:
-
trapTarget
: 代理的目标对象;
-
argumentsList
: 传入的参数数组
十.revocable
有时候由于安全原因或者取消某些功能而取消代理,可以使用Proxy.revocable()
方法,接受Proxy构造器相同的参数,返回一个对象,有如下2个属性:
-
proxy
: 能够取消代理的proxy object;
-
revoke
: the function to call to revoke the proxy;
当调用revoke()方法,则不能再对代理对象进行操作
let target = {
name: "james"
};
// 创建一个可取消代理的对象, 使用解构赋值
let {proxy, revoke} = Proxy.revocable(target, {});
console.log(proxy.name); // "james"
// 取消代理
revoke();
// 再操作抛出错误
console.log(proxy.name); // error
总结
Proxy的出现使我们能够定义一些对象的非标准行为,由于篇幅原因有很多使用功能暂未涉及,比如proxy对象当作原型, 还有模拟数组的索引等功能都未讲到,这些希望以后能够在补充。
-
实例解析ES6 Proxy使用场景 实际示例, 这篇文章说代理就像
中间件(middleware)
我感觉还比较贴切