JS Tip of the Day: Property Descriptors

Property Descriptors
Level: Intermediate

Fun fact: object properties have their own properties. These properties can’t be accessed through normal dot (or bracket) syntax like other properties are in JavaScript. Instead, they are exposed in a descriptor object obtained through the Object.getOwnPropertyDescriptor() function.

A property descriptor object contains normal properties that pertain to the described property and how it behaves. A property descriptor can have a number of different properties, though which depend on what kind of property was being described, a data property or an accessor property.

// data property descriptor
{ value, writable, enumerable, configurable }

// accessor property descriptor
{ get, set, enumerable, configurable }

Their meanings (ripped from MDN):

  • value: The value associated with the property.
  • writable: true if and only if the value associated with the property may be changed.
  • get: A function which serves as a getter for the property, or undefined if there is no getter.
  • set: A function which serves as a setter for the property, or undefined if there is no setter.
  • enumerable: true if and only if this property shows up during enumeration of the properties on the corresponding object.
  • configurable: true if and only if the type of this property descriptor may be changed and if the property may be deleted from the corresponding object.

The default values for those properties you don’t see through normal assignment is true.

let obj = {
    data: 1, // data property
    get accessor () {}, // accessor property
    set accessor (value) {},
};

console.log(Object.getOwnPropertyDescriptor(obj, 'data'));
// {value: 1, writable: true, enumerable: true, configurable: true}
console.log(Object.getOwnPropertyDescriptor(obj, 'accessor'));
// {get: ƒ, set: ƒ, enumerable: true, configurable: true}

You can change any of these values by passing a descriptor object through Object.defineProperty(). The descriptor object provided can be a full descriptor containing all of the property’s properties, or a partial containing only the properties you want to change.

let obj = {
    data: 1
};

obj.data = 2; // Ok
console.log(obj.data); // 2

Object.defineProperty(obj, 'data', { writable: false });
obj.data = 3; // silently fails or throws TypeError in strict mode
console.log(obj.data); // 2

Object.defineProperty() can also be used to create brand new properties on objects. However, be warned that the defaults for unspecified descriptor values for new properties are very restrictive, opposite, in fact, of what you get through normal assignment. Because of this, you’ll likely want to make sure you use a full descriptor, explicitly providing a value for each property. For existing properties, if not provided in the descriptor, the existing descriptor value for that property will be used (seen above).

let obj = {};
Object.defineProperty(obj, 'data', {}); // use default descriptor
console.log(Object.getOwnPropertyDescriptor(obj, 'data'));
// {value: undefined, writable: false, enumerable: false, configurable: false}

Batch versions of these functions also exist for setting or getting multiple property descriptors at once with Object.defineProperties() and Object.getOwnPropertyDescriptors().

let obj = {};
Object.defineProperties(obj, {
    hidden: { enumerable: true },
    editable: { value: 'text', configurable: true }
});
console.log(Object.getOwnPropertyDescriptors(obj));
/* {
    hidden: {
        value: undefined,
        writable: false,
        enumerable: true,
        configurable: false
    },
    locked: {
        value: "text",
        writable: false,
        enumerable: false,
        configurable: true
    }
} */

And if you’ve ever used Object.create(), you may have noticed that it’s second argument accepts a properties object like that used by Object.defineProperties() above.

More info: