深入理解 ES6 #12-代理(Proxy)和反射(Reflection)API
🏷️ 《深入理解 ES6》
代理(Proxy)是一种可以拦截并改变底层 JavaScript 引擎操作的包装器,在新语言中通过它暴露内部运作的对象,从而让开发者可以创建内建的对象。
数组问题
在 ECMAScript6 出现之前,开发者不能通过自己定义的对象模仿 JavaScript 数组对象的行为方式。当给数组的特定元素赋值时,影响到该数组的 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"
NOTE
数值属性和 length
属性具有这种非标准行为,因而在 ECMAScript 中数组被认为是奇异对象(exotic object,与普通对象相对)。
代理和反射
调用 new Proxy()
可创建代替其他目标(taget)对象的代理,它虚拟化了目标,所以二者看起来功能一致。
代理可以拦截 JavaScript 引擎内部目标的底层对象操作,这些底层操作被拦截后会触发响应特定操作的陷阱函数。
反射 API 可以 Reflect 对象的形式出现,对象中方法的默认特性与相同的底层操作一致,而代理可以覆写这些操作,每个代理陷阱对应一个命名和参数都相同的 Reflect 方法。
代理陷阱 | 覆写的特性 | 默认特性 |
---|---|---|
get | 读取一个属性值 | Reflect.get() |
set | 写入一个属性值 | Reflect.set() |
has | in 操作符 | Reflect.has() |
deleteProperty | delete 操作符 | Reflect.deleteProperty() |
getPrototypeOf | Object.getPrototypeOf() | Reflect.getPrototypeOf() |
setPrototypeOf | Object.setPrototypeOf() | Reflect.setPrototypeOf() |
isExtensible | Object.isExtensible() | Reflect.isExtensible() |
preventExtensions | Object.preventExtensions() | Reflect.preventExtensions() |
getOwnPropertyDescriptor | Object.getOwnPropertyDescriptor() | Reflect.getOwnPropertyDescriptor() |
defineProperty | Object.defineProperty() | Reflect.defineProperty() |
ownKeys | Object.keys() 、Object.getOwnPropertyNames() 和 Object.getOwnPropertySymbols() | Reflect.ownKeys() |
apply | 调用一个函数 | Reflect.apply() |
construct | 用 new 调用一个函数 | Reflect.construct() |
创建一个简单的代理
Proxy 构造函数有两个参数
- 目标(target)
- 处理程序(handler)
处理程序是定义一个或多个陷阱的对象,在代理中,出了专门为操作定义的陷阱外,其余操作均使用默认特性。
不使用任何陷阱的处理程序等价于简单的转发代理。
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"
使用 set 陷阱验证属性
set
陷阱接受 4 个参数:
trapTarget
用于接收属性(代理的目标)的对象key
要写入的属性键(字符串或 Symbol 类型)value
被写入属性的值receiver
操作发生的对象(通常是代理)
Reflect.set()
是 set
陷阱对应的反射方法和默认特性,它和 set
陷阱一样也接受同样的 4 个参数。
let target = {
name: "target"
};
let proxy = new Proxy(target, {
set(trapTarget, key, value, receiver) {
if (!trapTarget.hasOwnProperty(key)) {
if (isNaN(value)) {
throw new TypeError("属性必须是数字");
}
}
return Reflect.set(trapTarget, key, value, receiver);
}
});
proxy.count = 1;
console.log(proxy.count); // 1
console.log(target.count); // 1
proxy.name = "proxy";
console.log(proxy.name); // "proxy"
console.log(target.name); // "proxy"
// 抛出错误:
// Uncaught TypeError: 属性必须是数字
proxy.anotherName = "proxy";
用 get 陷阱验证对象结构(Object Shape)
get
陷阱接受 3 个参数:
trapTarget
被读取属性的源的对象(代理的目标)key
被读取的属性键value
操作发生的对象(通常是代理)
Reflect.get()
也接受同样 3 个参数并返回属性的默认值。
let proxy = new Proxy({}, {
get(trapTarget, key, receiver) {
if (!(key in receiver)) {
throw new TypeError("属性" + key + "不存在");
}
return Reflect.get(trapTarget, key, receiver);
}
});
proxy.name = "proxy";
console.log(proxy.name); // "proxy"
// 抛出错误:
// Uncaught TypeError: 属性 nme 不存在
console.log(proxy.nme);
使用 has 陷阱隐藏已有属性
可以用 in
操作符来检测给定对象中是否含有某个属性,如果自由属性或原型属性匹配的名称或 Symbol 就返回 true
。
let target = {
value: 42
};
console.log("value" in target); // true
console.log("toString" in target); // true
在代理中使用 has
陷阱可以拦截这些 in
操作并返回一个不同的值。
in
陷阱接受 2 个参数:
trapTarget
读取属性的对象(代理的目标)key
要检查的属性值(字符串或 Symbol)
let target = {
name: "target",
vlaue: 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
用 deleteProperty 陷阱防止删除属性
delete
操作符可以从对象中删除属性,如果成功则返回 true
,不成功则返回 false
。
在严格模式下,如果你尝试删除一个不可配置(nonconfigurable)属性则会导致程序抛出错误,而在非严格模式下只是返回 false
。
每当通过 delete
操作符删除对象属性时,deleteProperty
陷阱都会被调用,它接受 2 个参数:
trapTarget
要删除属性的对象(代理的目标)key
要删除的属性键(字符串或 Symbol)
Reflect.deleteProperty()
方法为 deleteProperty
陷阱提供默认实现,并且接受同样的两个参数。
let target = {
name: "target",
value: 42
};
let proxy = new Proxy(target, {
deleteProperty(trapTarget, key) {
if (key === "value") {
return false;
} else {
return Reflect.deleteProperty(trapTarget, key);
}
}
});
console.log("value" in proxy); // true
let result1 = delete proxy.value;
console.log(result1); // false
console.log("value" in proxy); // true
console.log("name" in proxy); // true
let result2 = delete proxy.name;
console.log(result2); // true
console.log("name" in proxy); // false
原型代理陷阱
ES6 中新增的 Object.setPrototypeOf()
方法,它被用于作为 ES5 中的 Object.getPrototype()
方法的补充。通过代理中的 setPrototypeOf
陷阱和 getPrototypeOf
陷阱可以拦截这两个方法的执行过程。
setPrototypeOf
陷阱接受 2 个参数:
trapTarget
接受原型设置的对象(代理的目标)proto
作为原型使用的对象
传入 Object.setPrototypeOf()
方法和 Reflect.setPrototypeOf()
方法的均是以上两个参数。
getPrototypeOf
陷阱、Object.getPrototypeOf()
方法和 Reflect.getPrototypeOf()
方法只接受参数 trapTarget
。
原型代理陷阱的运行机制
原型代理陷阱有一些限制:
getPrototypeOf
陷阱必须返回对象或null
在
setPrototypeOf
陷阱中,如果操作失败则返回的一定是false
,此时Object.setPrototypeOf()
会抛出错误,如果setPrototypeOf
返回了任何不是false
的值,那么Object.setPrototypeOf()
便假设操作成功。
let target = {};
let proxy = new Proxy(target, {
getPrototypeOf(trapTarget) {
return null;
},
setPrototypeOf(trapTarget, proto) {
return false;
}
});
let targetProto = Object.getPrototypeOf(target);
let proxyProto = Object.getPrototypeOf(proxy);
console.log(targetProto === Object.prototype); // true
console.log(proxyProto === Object.prototype); // false
console.log(proxyProto); // null
// 成功
Object.setPrototypeOf(target, {});
// 给不存在的属性赋值会抛出错误:
// Uncaught TypeError: 'setPrototypeOf' on proxy: trap returned falsish
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);
}
});
let targetProto = Object.getPrototypeOf(target);
let proxyProto = Object.getPrototypeOf(proxy);
console.log(targetProto === Object.prototype); // true
console.log(proxyProto === Object.prototype); // true
// 成功
Object.setPrototypeOf(target, {});
// 成功:
Object.setPrototypeOf(proxy, {});
对象可扩展性陷阱
ECMAScript 5 已经通过 Object.preventExtensions()
方法和 Object.isExtensible()
方法修正了对象的可扩展性;
ECMAScript 6 可以通过代理中的 preventExtensions
和 isExtensible
陷阱拦截这两个方法并调用底层对象。
两个陷阱都接受唯一参数 trapTarget
对象,并调用它上面的方法。
isExtensible
陷阱返回的一定是一个 boolean
值,表示对象是否可扩展;preventExtensions
陷阱返回的也一定是布尔值,表示操作是否成功。
Reflect.preventExtensions()
方法和 Reflect.isExtensible()
方法实现了相应陷阱中的默认行为,二两都返回布尔值。
两个基础示例
默认实现:
let target = {};
let proxy = new Proxy(target, {
isExtensible(trapTarget) {
return Reflect.isExtensible(trapTarget);
},
preventExtensions(trapTarget) {
return Reflect.preventExtensions(trapTarget);
}
});
console.log(Object.isExtensible(target)); // true
console.log(Object.isExtensible(proxy)); // true
Object.preventExtensions(proxy);
console.log(Object.isExtensible(target)); // false
console.log(Object.isExtensible(proxy)); // false
使用陷阱使 Object.preventExtensions()
对 proxy
失效。
let target = {};
let proxy = new Proxy(target, {
isExtensible(trapTarget) {
return Reflect.isExtensible(trapTarget);
},
preventExtensions(trapTarget) {
return false;
}
});
console.log(Object.isExtensible(target)); // true
console.log(Object.isExtensible(proxy)); // true
// 抛出错误:
// Uncaught TypeError: 'preventExtensions' on proxy: trap returned falsish
Object.preventExtensions(proxy);
console.log(Object.isExtensible(target)); // true
console.log(Object.isExtensible(proxy)); // true
属性描述符陷阱
ECMAScript 5 最重要的特性之一是可以使用 Object.defineProperty()
方法定义属性特性(property attribute)。可以通过 Object.getOwnPropertyDescriptor()
方法来获取这些属性。
在代理中可以分别用 defineProperty
陷阱和 getOwnPropertyDescriptor
陷阱拦截 Object.defineProperty()
方法和 Object.getOwnPropertyDescriptor()
方法的调用。
defineProperty
陷阱接受以下参数:
trapTarget
要定义属性的对象(代理的目标)key
属性的键(字符串或 Symbol)descriptor
属性的描述符对象
操作成功后返回 true
,否则返回 false
。
getOwnPropertyDescriptor
陷阱接受以下参数:
trapTarget
要定义属性的对象(代理的目标)key
属性的键(字符串或 Symbol)
最终返回描述符。
陷阱默认行为示例:
let proxy = new Proxy({}, {
defineProperty(trapTarget, key, descriptor) {
return Reflect.defineProperty(trapTarget, key, descriptor);
},
getOwnPropertyDescriptor(trapTarget, key) {
return Reflect.getOwnPropertyDescriptor(trapTarget, key);
}
});
Object.defineProperty(proxy, "name", {
value: "proxy"
});
console.log(proxy.name); // "proxy"
let descriptor = Object.getOwnPropertyDescriptor(proxy, "name");
console.log(descriptor.value); // "proxy"
给 Object.defineProperty() 添加限制
defineProperty
陷阱返回布尔值来表示操作是否成功。
返回 true
时,Object.defineProperty()
方法成功执行;
返回 false
时,Object.defineProperty()
方法抛出错误。
例:阻止 Symbol 类型的属性
let proxy = new Proxy({}, {
defineProperty(trapTarget, key, descriptor) {
if (typeof key === "symbol") {
return false;
}
return Reflect.defineProperty(trapTarget, key, descriptor);
}
});
Object.defineProperty(proxy, "name", {
value: "proxy"
});
console.log(proxy.name); // "proxy"
let nameSymbol = Symbol("name");
// 抛出错误:
// Uncaught TypeError: 'defineProperty' on proxy: trap returned falsish for property 'Symbol(name)'
Object.defineProperty(proxy, nameSymbol, {
value: "proxy"
});
描述符对象限制
defineProperty 陷阱
defineProperty
陷阱的描述对象已规范化,只有下列属性会被传递给 defineProperty
陷阱的描述符对象。
enumerable
configurable
value
writable
get
set
let proxy = new Proxy({}, {
defineProperty(trapTarget, key, descriptor) {
console.log(descriptor.value); // "proxy"
console.log(descriptor.name); // undefined
return Reflect.defineProperty(trapTarget, key, descriptor);
}
});
Object.defineProperty(proxy, "name", {
value: "proxy",
name: "custom"
});
getOwnPropertyDescriptor 陷阱
getOwnPropertyDescriptor
陷阱的返回值必须是 null
、undefined
或一个对象;
如果返回对象,则对象自己的属性只能是 enumerable
、configurable
、vlaue
、writable
、get
和 set
;
在返回的对象中使用不被允许的属性会抛出一个错误。
let proxy = new Proxy({}, {
getOwnPropertyDescriptor(trapTarget, key) {
return {
name: "proxy"
};
}
});
// 给不存在的属性赋值会抛出错误
// Uncaught TypeError: 'getOwnPropertyDescriptor' on proxy: trap reported non-configurability for property 'name' which is either non-existant or configurable in the proxy target
let descriptor = Object.getOwnPropertyDescriptor(proxy, "name");
这条限制可以确保无论代理中使用了什么方法,Object.getOwnPropertyDescriptor()
返回值的结构总是可靠的。
ownKeys 陷阱
ownKeys
陷阱可以拦截内部方法 [[OwnPropertyKeys]]
,通过返回一个数组的值可以覆写其行为。
这个数组被用于 Object.keys()
、Object.getOwnPropertyNames()
、Object.getOwnPropertySymbols()
和 Object.assign()
4 个方法,Object.assign()
方法用数组来确定需要复制的属性。
ownKeys
陷阱通过 Reflect.ownKeys()
方法实现默认的行为,返回的数组中包含所有自有属性的键名,字符串类型和 Symbol 类型的都包含在内。
Object.getOwnPropertyNames()
方法和 Object.keys()
方法返回的结果将 Symbol 类型的属性名排除在外;Object.getOwnPropertySymbols()
方法返回的结果将字符串类型的属性名排除在外;Object.assign()
方法支持字符串和 Symbol 两种类型。
ownKeys
陷阱唯一接受的参数是操作的目标,返回值必须是一个数组或类数组对象,否则就抛出错误。
例:过滤任何以下划线字符开头的属性名称。
let proxy = new Proxy({}, {
ownKeys(trapTarget) {
return Reflect.ownKeys(trapTarget).filter(key => {
return typeof key !== "string" || key[0] !== "_";
});
}
});
let nameSymbol = Symbol("name");
proxy.name = "proxy";
proxy._name = "private";
proxy[nameSymbol] = "symbol";
let names = Object.getOwnPropertyNames(proxy),
keys = Object.keys(proxy),
symbols = Object.getOwnPropertySymbols(proxy);
console.log(names.length); // 1
console.log(names[0]); // "name"
console.log(keys.length); // 1
console.log(keys[0]); // "name"
console.log(symbols.length); // 1
console.log(symbols[0]); // "Symbol(name)"
函数代理的 apply 和 construct 陷阱
所有代理陷阱中,只有 apply
和 construct
的代理目标是一个函数。
函数有两个内部方法 [[Call]]
和 [[Construct]]
,apply
陷阱和 construct
陷阱可以覆写这些内部方法。
若使用 new
操作符调用函数,则执行 [[Construct]]
方法;若不用,则执行 [[Call]]
方法。
apply
陷阱和 Reflect.apply()
都接受以下参数:
trapTarget
被执行的函数(代理的目标)thisArg
函数被调用时内部 this 的值argumentList
传递给函数的参数数组
当使用 new
调用函数时调用的 construct
陷阱接受以下参数:
trapTarget
被执行的函数(代理的目标)argumentList
传递给函数的参数数组
Reflect.construct()
方法也接受这两个参数,其还有一个可选的第三个参数 newTarget。
let target = function() { return 42; },
proxy = new Proxy(target, {
apply: function(trapTarget, thisArg, argumentList) {
return Reflect.apply(trapTarget, thisArg, argumentList);
},
construct: function(trapTarget, argumentList) {
return Reflect.construct(trapTarget, argumentList);
}
});
// 一个目标是函数的代理开起来也像一个函数
console.log(typeof proxy); // function
console.log(proxy()); // 42
var instance = new proxy();
console.log(instance instanceof proxy); // true
console.log(instance instanceof target); // true
验证函数参数
例:验证所有参数必须是数字:
// 将所有参数相加
function sum(...values) {
return values.reduce((previous, current) => previous + current, 0);
}
let sumProxy = new Proxy(sum, {
apply: function(trapTarget, thisArg, argumentList) {
argumentList.forEach((arg) => {
if (typeof arg !== "number") {
throw new TypeError("所有参数必须是数字");
}
});
return Reflect.apply(trapTarget, thisArg, argumentList);
},
construct: function(trapTarget, argumentList) {
throw new TypeError("该函数不可通过 new 来调用");
}
});
console.log(sumProxy(1, 2, 3, 4)); // 10
// 抛出错误
// Uncaught TypeError: 所有参数必须是数字
console.log(sumProxy(1, "2", 3, 4));
// 抛出错误
// Uncaught TypeError: 该函数不可通过 new 来调用
let result = new sumProxy();
可调用的类构造函数
使用 apply 陷阱创建实例
class Person {
constructor(name) {
this.name = name;
}
}
let PersonProxy = new Proxy(Person, {
apply: function(trapTarget, thisArg, argumentList) {
return new trapTarget(...argumentList);
}
});
let me = PersonProxy("JiaJia");
console.log(me.name); // JiaJia
console.log(me instanceof Person); // true
console.log(me instanceof PersonProxy); // true
可撤销代理
可以使用 Proxy.revocable() 方法创建可撤销的代理,该方法采用与 Proxy 构造函数相同的参数:目标对象和代理处理程序。
返回值是具有以下属性的对象:
- proxy
可被撤销的代理对象 - revoke
撤销代理要调用的函数
let target = {
name: "target"
};
let { proxy, revoke } = Proxy.revocable(target, {});
console.log(proxy.name); // target
revoke();
// 抛出错误
// Uncaught TypeError: Cannot perform 'get' on a proxy that has been revoked
console.log(proxy.name);
解决数组问题
数组问题
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;
自定义数组类型
function toUint32(value) {
return Math.floor(Math.abs(Number(value))) % Math.pow(2, 32);
}
function isArrayIndex(key) {
let numericKey = toUint32(key);
return String(numericKey) == key && numericKey < (Math.pow(2, 32) - 1);
}
class MyArray {
constructor(length = 0) {
this.length = length;
return new Proxy(this, {
set(trapTarget, key, value) {
let currentLength = Reflect.get(trapTarget, "length");
if (isArrayIndex(key)) {
let numericKey = Number(key);
if (numericKey >= currentLength) {
Reflect.set(trapTarget, "length", numericKey + 1);
}
} else if (key === "length") {
if (value < currentLength) {
for (let index = currentLength - 1; index >= value; index--) {
Reflect.deleteProperty(trapTarget, index);
}
}
}
return Reflect.set(trapTarget, key, value);
}
});
}
}
let colors = new MyArray(3);
console.log(colors instanceof MyArray); // true
console.log(colors.length); // 3
let colors2 = new MyArray(5);
console.log(colors.length); // 3
console.log(colors2.length); // 5
colors[0] = "red";
colors[1] = "green";
colors[2] = "blue";
colors[3] = "black";
console.log(colors.length); // 4
colors.length = 2;
console.log(colors.length); // 2
console.log(colors[3]); // undefined
console.log(colors[2]); // undefined
console.log(colors[1]); // "green"
console.log(colors[0]); // "red"
将代理作为原型
let target = {};
let proxy = new Proxy(target, {
defineProperty(trapTarget, name, descriptor) {
return false;
}
});
let newTarget = Object.create(proxy);
Object.defineProperty(newTarget, "name", {
value: "newTarget"
});
console.log(newTarget.name); // "newTarget"
console.log(newTarget.hasOwnProperty("name")); // true
console.log(Object.getPrototypeOf(newTarget) === proxy); // true
关于 Object.create() 方法可以参照 这里
上例中 newTarget 的原型是代理,但是在对象上定义属性的操作不需要操作对象原型,所以没有触发代理中的陷阱。
尽管代理作为原型使用时及其受限,但有几个陷阱仍然有用。
在原型上使用 get 陷阱
let target = {};
let thing = Object.create(new Proxy(target, {
get(trapTarget, key, value) {
throw new ReferenceError(`${key} deesn't exist`);
}
}));
thing.name = "thing";
console.log(thing.name); // "thing"
// 抛出异常:
// Uncaught ReferenceError: unknown deesn't exist
let unknown = thing.unknown;
访问对象上不存在的属性时,会触发原型中的 get 陷阱。
在原型上使用 set 陷阱
let target = {};
let thing = Object.create(new Proxy(target, {
set (trapTarget, key, value, receiver) {
return Reflect.set(trapTarget, key, value, receiver);
}
}));
console.log(thing.hasOwnProperty("name")); // false
// 触发 set 陷阱
thing.name = "thing";
console.log(thing.name); // "thing"
console.log(thing.hasOwnProperty("name")); // true
// 不触发 set 陷阱
thing.name = "boo";
console.log(thing.name); // "boo"
在原型上使用 has 陷阱
let target = {}
let thing = Object.create(new Proxy(target, {
has (trapTarget, key) {
return Reflect.has(trapTarget, key);
}
}));
// 触发 has 陷阱
console.log("name" in thing); // false
thing.name = "thing";
// 不触发 has 陷阱
console.log("name" in thing); // true
第一次 in 操作符触发 has 陷阱,是因为 name 不是 thing 的自有属性。
将代理用作类的原型
function NoSuchProperty() {
}
let proxy = new Proxy({}, {
get (trapTarget, key, receiver) {
throw new ReferenceError(`${key} doesn't exist`);
}
});
NoSuchProperty.prototype = proxy;
class Square extends NoSuchProperty {
constructor(length, width) {
super();
this.length = length;
this.width = width;
}
}
let shape = new Square(2, 6);
let shapeProto = Object.getPrototypeOf(shape);
console.log(shapeProto === proxy); // false
let secondLevelProto = Object.getPrototypeOf(shapeProto);
console.log(secondLevelProto === proxy); // true
let area1 = shape.length * shape.width;
console.log(area1); // 12
// 由于 wdth 不存在,抛出错误:
// Uncaught ReferenceError: wdth doesn't exist
let area2 = shape.length * shape.wdth;