属性描述符的陷阱函数
ES5 最重要的特征之一就是引入了 Object.defineProperty() 方法用于定义属性的特性。 在 JS 之前的版本中, 没有方法可以定义一个访问器属性, 也不能让属性变成只读或是不可枚举。 而这些特性都能够利用 Object.defineProperty() 方法来实现, 并且你还可以利用 Object.getOwnPropertyDescriptor() 方法来检索这些特性。
代理允许你使用 defineProperty 与 getOwnPropertyDescriptor 陷阱函数, 来分别拦截对于 Object.defineProperty() 与 Object.getOwnPropertyDescriptor() 的调用。 defineProperty 陷阱函数接受下列三个参数:
-
trapTarget:需要被定义属性的对象(即代理的目标对象);
-
key:属性的键(字符串类型或符号类型);
-
descriptor:为该属性准备的描述符对象。
defineProperty 陷阱函数要求你在操作成功时返回 true,否则返回 false。getOwnPropertyDescriptor 陷阱函数则只接受 trapTarget 与 key 这两个参数,并会返回对应的描述符。Reflect.defineProperty() 与 Reflect.getOwnPropertyDescriptor() 方法作为上述陷阱函数的对应方法,接受与之相同的参数。这里有个例子,实现了每个陷阱函数的默认行为:
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() 方法在代理对象上定义了名为 "name" 的属性,该属性的描述符可以使用 Object.getOwnPropertyDescriptor() 方法进行检索。
阻止 Object.defineProperty()
defineProperty 陷阱函数要求你返回一个布尔值用于表示操作是否已成功。当它返回 true 时,Object.defineProperty() 会正常执行;而如果它返回了 false,则 Object.defineProperty() 会抛出错误。你可以使用该功能来限制哪些属性可以被 Object.defineProperty() 方法定义。例如,如果想阻止定义符号类型的属性,你可以检查传入的键是否为字符串,若不是则返回 false,就像这样:
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");
// throws error
Object.defineProperty(proxy, nameSymbol, {
value: "proxy"
});
当 key 是一个符号时,defineProperty 代理陷阱会返回 false,而其他情况下则会保持默认的行为。当使用字符串 "name" 作为键去调用 Object.defineProperty() 时,该方法能够成功执行;然而当使用符号变量 nameSymbol 去调用 Object.defineProperty() 的时候,defineProperty 陷阱函数返回了 false,导致程序抛出了错误。
你可以让陷阱函数返回 true,同时不去调用 Reflect.defineProperty() 方法,这样 Object.defineProperty() 就会静默失败,如此便可在未实际去定义属性的情况下抑制运行错误。 |
描述符对象的限制
为了确保 Object.defineProperty() 与 Object.getOwnPropertyDescriptor() 方法的行为一致,传递给 defineProperty 陷阱函数的描述符对象必须是正规的。出于同一原因,getOwnPropertyDescriptor 陷阱函数返回的对象也始终需要被验证。
任意对象都能作为 Object.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"
});
此代码中调用 Object.defineProperty() 时,在第三个参数上使用了一个非标准的 name 属性。当 defineProperty 陷阱函数被调用时,descriptor 对象不会拥有 name 属性,却拥有一个 value 属性。这是因为 descriptor 对象实际上并不是原先传递给 Object.defineProperty() 方法的第三个参数,而是一个新的对象,其中只包含了被许可的属性(因此 name 属性被丢弃了)。 Reflect.defineProperty() 方法同样也会忽略描述符上的非标准属性。
getOwnPropertyDescriptor 陷阱函数有一个微小差异,要求返回值必须是 null、undefined,或者是一个对象。如果返回值是一个对象,则只允许该对象拥有 enumerable、configurable、value、writable、get 或 set 这些自有属性。如果你返回的对象包含了不被许可的自有属性,则程序会抛出错误,就像下面演示的这样:
let proxy = new Proxy({}, {
getOwnPropertyDescriptor(trapTarget, key) {
return {
name: "proxy"
};
}
});
// throws error
let descriptor = Object.getOwnPropertyDescriptor(proxy, "name");
name 属性在属性描述符中是不被许可的,因此当 Object.getOwnPropertyDescriptor() 被调用时,getOwnPropertyDescriptor 的返回值会触发一个错误。这个限制保证了 Object.getOwnPropertyDescriptor() 的返回值总是拥有可信任的结构,无论是否使用了代理。
重复的描述符方法
ES6 再次出现了令人困惑的相似方法,Object.defineProperty() 和 Object.getOwnPropertyDescriptor() 方法貌似分别与 Reflect.defineProperty() 和 Reflect.getOwnPropertyDescriptor() 方法相同。正如本章之前讨论过的那些配套方法一样,这些方法也存在一些微小但重要的差异。
defineProperty() 方法
Object.defineProperty() 方法与 Reflect.defineProperty() 方法几乎一模一样,只是返回值有区别。前者返回调用它时的第一个参数,而后者在操作成功时返回 true、失败时返回 false。例如:
let target = {};
let result1 = Object.defineProperty(target, "name", { value: "target "});
console.log(target === result1); // true
let result2 = Reflect.defineProperty(target, "name", { value: "reflect" });
console.log(result2); // true
使用 target 对象去调用 Object.defineProperty() 方法,返回值也是 target。而同样使用 target 对象去调用 Reflect.defineProperty() , 返回值却是 true,表示操作已经成功。由于 defineProperty 代理陷阱需要一个布尔值作为返回值,因此最好在必要时使用 Reflect.defineProperty() 来实现默认的行为。
getOwnPropertyDescriptor() 方法
Object.getOwnPropertyDescriptor() 方法会在接收的第一个参数是一个基本类型值时,将该参数转换为一个对象。另一方面,Reflect.getOwnPropertyDescriptor() 方法则会在第一个参数是基本类型值的时候抛出错误。下面这个例子展示了二者的特性:
let descriptor1 = Object.getOwnPropertyDescriptor(2, "name");
console.log(descriptor1); // undefined
// throws an error
let descriptor2 = Reflect.getOwnPropertyDescriptor(2, "name");
此代码中的 Object.getOwnPropertyDescriptor() 方法返回了 undefined ,因为它将 2 转换为一个对象,转换后的对象并不包含 name 属性,而返回 undefined 是指定属性名在目标对象中不存在时的标准行为。然而当 Reflect.getOwnPropertyDescriptor() 被调用时,立刻抛出了一个错误,因为该方法不接受基本类型值作为它的第一个参数。