Property Descriptor Traps
One of the most important features of ECMAScript 5 was the ability to define property attributes using the Object.defineProperty()
method. In previous versions of JavaScript, there was no way to define an accessor property, make a property read-only, or make a property nonenumerable. All of these are possible with the Object.defineProperty()
method, and you can retrieve those attributes with the Object.getOwnPropertyDescriptor()
method.
Proxies let you intercept calls to Object.defineProperty()
and Object.getOwnPropertyDescriptor()
using the defineProperty
and getOwnPropertyDescriptor
traps, respectively. The defineProperty
trap receives the following arguments:
trapTarget
- the object on which the property should be defined (the proxy’s target)key
- the string or symbol for the propertydescriptor
- the descriptor object for the property
The defineProperty
trap requires you to return true
if the operation is successful and false
if not. The getOwnPropertyDescriptor
traps receives only trapTarget
and key
, and you are expected to return the descriptor. The corresponding Reflect.defineProperty()
and Reflect.getOwnPropertyDescriptor()
methods accept the same arguments as their proxy trap counterparts. Here’s an example that just implements the default behavior for each trap:
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"
This code defines a property called "name"
on the proxy with the Object.defineProperty()
method. The property descriptor for that property is then retrieved by the Object.getOwnPropertyDescriptor()
method.
Blocking Object.defineProperty()
The defineProperty
trap requires you to return a boolean value to indicate whether the operation was successful. When true
is returned, Object.defineProperty()
succeeds as usual; when false
is returned, Object.defineProperty()
throws an error. You can use this functionality to restrict the kinds of properties that the Object.defineProperty()
method can define. For instance, if you want to prevent symbol properties from being defined, you could check that the key is a string and return false
if not, like this:
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"
});
The defineProperty
proxy trap returns false
when key
is a symbol and otherwise proceeds with the default behavior. When Object.defineProperty()
is called with "name"
as the key, the method succeeds because the key is a string. When Object.defineProperty()
is called with nameSymbol
, it throws an error because the defineProperty
trap returns false
.
I> You can also have Object.defineProperty()
silently fail by returning true
and not calling the Reflect.defineProperty()
method. That will suppress the error while not actually defining the property.
Descriptor Object Restrictions
To ensure consistent behavior when using the Object.defineProperty()
and Object.getOwnPropertyDescriptor()
methods, descriptor objects passed to the defineProperty
trap are normalized. Objects returned from getOwnPropertyDescriptor
trap are always validated for the same reason.
No matter what object is passed as the third argument to the Object.defineProperty()
method, only the properties enumerable
, configurable
, value
, writable
, get
, and set
will be on the descriptor object passed to the defineProperty
trap. For example:
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"
});
Here, Object.defineProperty()
is called with a nonstandard name
property on the third argument. When the defineProperty
trap is called, the descriptor
object doesn’t have a name
property but does have a value
property. That’s because descriptor
isn’t a reference to the actual third argument passed to the Object.defineProperty()
method, but rather a new object that contains only the allowable properties. The Reflect.defineProperty()
method also ignores any nonstandard properties on the descriptor.
The getOwnPropertyDescriptor
trap has a slightly different restriction that requires the return value to be null
, undefined
, or an object. If an object is returned, only enumerable
, configurable
, value
, writable
, get
, and set
are allowed as own properties of the object. An error is thrown if you return an object with an own property that isn’t allowed, as this code shows:
let proxy = new Proxy({}, {
getOwnPropertyDescriptor(trapTarget, key) {
return {
name: "proxy"
};
}
});
// throws error
let descriptor = Object.getOwnPropertyDescriptor(proxy, "name");
The property name
isn’t allowable on property descriptors, so when Object.getOwnPropertyDescriptor()
is called, the getOwnPropertyDescriptor
return value triggers an error. This restriction ensures that the value returned by Object.getOwnPropertyDescriptor()
always has a reliable structure regardless of use on proxies.
Duplicate Descriptor Methods
Once again, ECMAScript 6 has some confusingly similar methods, as the Object.defineProperty()
and Object.getOwnPropertyDescriptor()
methods appear to do the same thing as the Reflect.defineProperty()
and Reflect.getOwnPropertyDescriptor()
methods, respectively. Like other method pairs discussed earlier in this chapter, these have some subtle but important differences.
defineProperty() Methods
The Object.defineProperty()
and Reflect.defineProperty()
methods are exactly the same except for their return values. The Object.defineProperty()
method returns the first argument, while Reflect.defineProperty()
returns true
if the operation succeeded and false
if not. For example:
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
When Object.defineProperty()
is called on target
, the return value is target
. When Reflect.defineProperty()
is called on target
, the return value is true
, indicating that the operation succeeded. Since the defineProperty
proxy trap requires a boolean value to be returned, it’s better to use Reflect.defineProperty()
to implement the default behavior when necessary.
getOwnPropertyDescriptor() Methods
The Object.getOwnPropertyDescriptor()
method coerces its first argument into an object when a primitive value is passed and then continues the operation. On the other hand, the Reflect.getOwnPropertyDescriptor()
method throws an error if the first argument is a primitive value. Here’s an example showing both:
let descriptor1 = Object.getOwnPropertyDescriptor(2, "name");
console.log(descriptor1); // undefined
// throws an error
let descriptor2 = Reflect.getOwnPropertyDescriptor(2, "name");
The Object.getOwnPropertyDescriptor()
method returns undefined
because it coerces 2
into an object, and that object has no name
property. This is the standard behavior of the method when a property with the given name isn’t found on an object. When Reflect.getOwnPropertyDescriptor()
is called, however, an error is thrown immediately because that method doesn’t accept primitive values for the first argument.