将代理对象作为原型使用
代理对象可以被作为原型使用,但这么做会比本章前面的例子更复杂一些。在把代理对象作为原型时,仅当操作的默认行为会按惯例追踪原型时,代理陷阱才会被调用,这就限制了代理对象作为原型时的能力。考虑这个例子:
let target = {};
let newTarget = Object.create(new Proxy(target, {
// never called
defineProperty(trapTarget, name, descriptor) {
// would cause an error if called
return false;
}
}));
Object.defineProperty(newTarget, "name", {
value: "newTarget"
});
console.log(newTarget.name); // "newTarget"
console.log(newTarget.hasOwnProperty("name")); // true
一个代理被作为原型创建了 newTarget 对象。将 target 作为代理的目标对象, 有效地让 target 成为了 newTarget 的原型,因为该代理是透明的。此时, 只有当 newTarget 将操作传递给 target 的时候,代理陷阱才会被调用。
Object.defineProperty() 方法在 newTarget 上被调用,创建了一个自有属性 name。定义对象属性的操作并不会按惯例追踪对象原型,因此代理上的 defineProperty 陷阱函数永远不会被调用,于是 name 属性就被添加到了 newTarget 对象上,成为它的一个自有属性。
尽管在把代理对象作为原型时会受到严重限制,但仍然存在几个很有用的陷阱函数。
在原型上使用 get 陷阱函数
得益于这个流程,若你设置了一个 get 代理陷阱,则只有在对象不存在指定名称的自有属性时,该陷阱函数才会在对象的原型上被调用。当所访问的属性无法保证存在时,你可以使用 get 陷阱函数来阻止预期外的行为。下例创建了一个对象,当你尝试去访问一个不存在的属性时,它会抛出错误:
let target = {};
let thing = Object.create(new Proxy(target, {
get(trapTarget, key, receiver) {
throw new ReferenceError(`${key} doesn't exist`);
}
}));
thing.name = "thing";
console.log(thing.name); // "thing"
// throw an error
let unknown = thing.unknown;
这段代码创建了一个将代理作为原型的 thing 对象。当 thing 对象中不存在指定键的时候,get 陷阱函数就会抛出错误。在读取 thing.name 时,因为该属性存在于 thing 对象中,get 陷阱函数没有被调用;而当读取不存在的 thing.unknown 属性时,get 陷阱函数才被调用了。
当最后一行代码执行时,unknown 并不是 thing 的自有属性,因此查找操作延续到了它的原型上,于是 get 陷阱函数抛出了一个错误。这种自定义行为对 JS 来说是非常有用的,因为它能够让 JS 像其他语言那样、在访问不存在的属性时抛出错误,而不是静默地返回 undefined 。
trapTarget 与 receiver 是不同的对象,这对理解本例是非常重要的。当代理被用作原型时,trapTarget 是原型对象自身,而 receiver 则是实例对象。这意味着在本例中,trapTarget 等于 target,而 receiver 则等于 thing。这就使得你既能访问代理的原始目标对象,也能访问操作将要涉及的对象。
在原型上使用 set 陷阱函数
内部方法 同样会查找对象的自有属性,并在必要时继续对该对象的原型进行查找。当你对一个对象属性进行赋值时,如果指定名称的自有属性存在,值就会被赋在该属性上;而若该自有属性不存在,则会继续检查对象的原型。微妙之处在于: 尽管赋值操作在原型上继续进行,但默认情况下它会在对象实例(而非原型)上创建一个新的属性用于赋值,无论同名属性是否存在于原型上。
为了更好地了解 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
// triggers the `set` proxy trap
thing.name = "thing";
console.log(thing.name); // "thing"
console.log(thing.hasOwnProperty("name")); // true
// does not trigger the `set` proxy trap
thing.name = "boo";
console.log(thing.name); // "boo"
在本例中,target 对象起初未拥有任何自有属性。thing 对象把一个代理作为自身的原型,并定义了一个 set 陷阱函数来捕获任意创建新属性的操作。当 thing.name 被赋值为 "thing" 时,因为 thing 对象并不存在一个名为 name 的自有属性,set 代理陷阱就被调用。在 set 陷阱函数中,trapTarget 参数等于 target,而 receiver 参数则等于 thing。你可以将 receiver 作为第四个参数传递给 Reflect.set() 方法来实现默认的行为,最终一个新的属性就在 thing 对象上被创建了。
在原型上使用 has 陷阱函数
可以回忆一下,has 陷阱函数会拦截对象上 in 运算符的使用。in 运算符首先查找对象上指定名称的自有属性;如果不存在同名自有属性,则会继续查找对象的原型;如果原型上也不存在同名自有属性,那么就会沿着原型链一直查找下去,直到找到该属性、 或者没有更多原型可供查找时为止。
has 陷阱函数只在原型链查找触及原型对象的时候才会被调用。当使用代理作为原型时,这只会在指定名称的自有属性不存在时发生。例如:
let target = {};
let thing = Object.create(new Proxy(target, {
has(trapTarget, key) {
return Reflect.has(trapTarget, key);
}
}));
// triggers the `has` proxy trap
console.log("name" in thing); // false
thing.name = "thing";
// does not trigger the `has` proxy trap
console.log("name" in thing); // true
此代码在 thing 的原型上创建了一个 has 代理陷阱。has 陷阱函数并没有像 get 或 set 陷阱函数那样传递一个 receiver 参数,因为当 in 运算符被使用时,对原型的查找是自动的。相反的,has 陷阱函数只能对 trapTarget 参数进行操作,该参数等于 target。本例中第一次使用 in 运算符的时候,由于 thing 并不存在自有属性 name,于是 has 陷阱函数就被调用了。而当 thing.name 被赋值之后,再次使用 in 运算符,has 陷阱函数则不会被调用,因为操作在找到 thing 的自有属性 name 后便已停止。
这里的原型范例都围绕着使用 Object.create() 方法创建的对象。然而若你想创建一个以代理为原型的对象,流程会有些不同。
将代理作为类的原型
类不能直接被修改为将代理用作自身的原型,因为它们的 prototype 属性是不可写入的。然而你可以使用一点变通手段,利用继承来创建一个把代理作为自身原型的类。首先你需要使用构造器函数创建一个 ES5 风格的类定义。你可以将原型改写为一个代理,这里有个例子:
function NoSuchProperty() {
// empty
}
NoSuchProperty.prototype = new Proxy({}, {
get(trapTarget, key, receiver) {
throw new ReferenceError(`${key} doesn't exist`);
}
});
let thing = new NoSuchProperty();
// throws error due to `get` proxy trap
let result = thing.name;
NoSuchProperty 函数代表了将会被用于继承的基础类。此函数的 prototype 属性不存在任何限制,因此你可以将其改写为一个代理,其中 get 陷阱函数被用于在属性缺失时抛出错误。thing 对象被创建为 NoSuchProperty 类的一个实例,当访问不存在的 name 属性时,错误就被抛出。
下一步是创建一个继承 NoSuchProperty 的类。你可以简单使用第九章介绍过的 extends 语法,来将代理引入该类的原型链,就像这样:
function NoSuchProperty() {
// empty
}
NoSuchProperty.prototype = new Proxy({}, {
get(trapTarget, key, receiver) {
throw new ReferenceError(`${key} doesn't exist`);
}
});
class Square extends NoSuchProperty {
constructor(length, width) {
super();
this.length = length;
this.width = width;
}
}
let shape = new Square(2, 6);
let area1 = shape.length * shape.width;
console.log(area1); // 12
// throws an error because "wdth" doesn't exist
let area2 = shape.length * shape.wdth;
Square 类继承了 NoSuchProperty 类,因此该代理就被加入了 Square 类的原型链。随后 shape 对象被创建为 Square 类的一个实例,让它拥有两个属性: length 与 width。由于 get 陷阱函数永远不会被调用,因此能够成功读取这两个属性的值。只有访问 shape 上不存在的属性时(例如这里的 shape.wdth 拼写错误),才触发了 get 陷阱函数并导致错误被抛出。
这证明了该代理存在于 shape 的原型链中,但这可能并不明显,因为该代理不是 shape 的直接原型。事实上,该代理需要用两步才能从 shape 的原型链上被找到。你可以修改前面的例子来更清晰地领会这一点:
function NoSuchProperty() {
// empty
}
// store a reference to the proxy that will be the prototype
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
这个版本的代码将代理存储在一个名为 proxy 的变量中,以便之后可以简单识别。 shape 的原型是 Shape.prototype,它并不是一个代理。然而 Shape.prototype 的原型却是一个从 NoSuchProperty 继承下来的代理。
继承行为在原型链上增加了一步, 明白这一点很重要,因为在 proxy 变量上调用 get 陷阱函数的操作也需要多进行一步。如果欲使用的属性存在于 Shape.prototype 上,那么这就会防止 get 代理陷阱被调用,正如此例:
function NoSuchProperty() {
// empty
}
NoSuchProperty.prototype = new Proxy({}, {
get(trapTarget, key, receiver) {
throw new ReferenceError(`${key} doesn't exist`);
}
});
class Square extends NoSuchProperty {
constructor(length, width) {
super();
this.length = length;
this.width = width;
}
getArea() {
return this.length * this.width;
}
}
let shape = new Square(2, 6);
let area1 = shape.length * shape.width;
console.log(area1); // 12
let area2 = shape.getArea();
console.log(area2); // 12
// throws an error because "wdth" doesn't exist
let area3 = shape.length * shape.wdth;
此处的 Square 类拥有一个 getArea() 方法,该方法被自动添加到 Square.prototype 上,因此当 shape.getArea() 被调用时,对于 getArea() 方法的查找从 shape 实例上开始,并延续到它的原型上。由于在原型上找到了 getArea() 方法,查找就停止了,代理也没有被调用。在本例的条件下,这正是你想要的行为,而 getArea() 被调用时抛出错误则是不正确的。
尽管使用了一点额外的代码来创建一个类,才让代理存在于该类的原型链上,但当你确实需要这样的功能时,这种付出仍然是值得的。