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:

  1. trapTarget - the object on which the property should be defined (the proxy’s target)
  2. key - the string or symbol for the property
  3. descriptor - 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:

  1. let proxy = new Proxy({}, {
  2. defineProperty(trapTarget, key, descriptor) {
  3. return Reflect.defineProperty(trapTarget, key, descriptor);
  4. },
  5. getOwnPropertyDescriptor(trapTarget, key) {
  6. return Reflect.getOwnPropertyDescriptor(trapTarget, key);
  7. }
  8. });
  9. Object.defineProperty(proxy, "name", {
  10. value: "proxy"
  11. });
  12. console.log(proxy.name); // "proxy"
  13. let descriptor = Object.getOwnPropertyDescriptor(proxy, "name");
  14. 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:

  1. let proxy = new Proxy({}, {
  2. defineProperty(trapTarget, key, descriptor) {
  3. if (typeof key === "symbol") {
  4. return false;
  5. }
  6. return Reflect.defineProperty(trapTarget, key, descriptor);
  7. }
  8. });
  9. Object.defineProperty(proxy, "name", {
  10. value: "proxy"
  11. });
  12. console.log(proxy.name); // "proxy"
  13. let nameSymbol = Symbol("name");
  14. // throws error
  15. Object.defineProperty(proxy, nameSymbol, {
  16. value: "proxy"
  17. });

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:

  1. let proxy = new Proxy({}, {
  2. defineProperty(trapTarget, key, descriptor) {
  3. console.log(descriptor.value); // "proxy"
  4. console.log(descriptor.name); // undefined
  5. return Reflect.defineProperty(trapTarget, key, descriptor);
  6. }
  7. });
  8. Object.defineProperty(proxy, "name", {
  9. value: "proxy",
  10. name: "custom"
  11. });

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:

  1. let proxy = new Proxy({}, {
  2. getOwnPropertyDescriptor(trapTarget, key) {
  3. return {
  4. name: "proxy"
  5. };
  6. }
  7. });
  8. // throws error
  9. 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:

  1. let target = {};
  2. let result1 = Object.defineProperty(target, "name", { value: "target "});
  3. console.log(target === result1); // true
  4. let result2 = Reflect.defineProperty(target, "name", { value: "reflect" });
  5. 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:

  1. let descriptor1 = Object.getOwnPropertyDescriptor(2, "name");
  2. console.log(descriptor1); // undefined
  3. // throws an error
  4. 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.