Property descriptors

The majority of your interaction with object properties will likely be surface-level, including creating object literals and setting and accessing property values using keys. However, you can internally configure any property of an object for fine-grained control over how those properties are accessed, altered, and defined. Every object property has a set of invisible attributes containing metadata associated with that property, called "property descriptors."

There are two types of descriptors associated with any property: data descriptors and accessor descriptors. A data descriptor includes key and value pairs that contain a property's value, regardless of whether that value is writable, configurable, or enumerable. Accessor descriptors contain functions that execute when a property is set, changed, or accessed.

Property Descriptor type Default value from
Object.defineProperty()
Description
[[Value]] Data undefined Contains a property's value.
[[Writable]] Data false Determines whether you can change the property's value.
[[Get]] Accessor undefined The property's getter function, which executes when the property is accessed.
[[Set]] Accessor undefined The property's setter function, which executes when the property is set or changed.
[[Configurable]] Both false If this is false, the property can't be deleted and its attributes can't be changed. If this is false and [[Writable]] is true, the property's value can still be changed.
[[Enumerable]] Both false If this is true, you can iterate over the property using for...in loops or the Object.keys() static method.

Each of these properties uses the same shorthand as [[Prototype]], indicating that these properties aren't meant to be accessed directly. Instead, use the Object.defineProperty() static method to define or modify the properties of an object. Object.defineProperty() accepts three arguments: the object to act on, the property key to be created or modified, and an object containing the descriptor(s) associated with the property being created or modified.

By default, properties created using Object.defineProperty() aren't writable, enumerable, or configurable. However, any property you create either as part of the object literal or using dot or bracket notation is writable, enumerable, and configurable.

const myObj = {};

Object.defineProperty(myObj, 'myProperty', {
  value: true,
  writable: false
});

myObj.myProperty;
> true

myObj.myProperty = false;

myObj.myProperty;
> true

For example, when [[Writable]] has a false value, trying to set a new value for the associated property fails silently outside strict mode, and throws an error in strict mode:

{
    const myObj = {};

    Object.defineProperty(myObj, 'myProperty', {
    value: true,
    writable: false
    });

    myObj.myProperty = false;
    myObj.myProperty;
}
> true

(function () {
    "use strict";
    const myObj = {};

    Object.defineProperty(myObj, 'myProperty', {
    value: true,
    writable: false
    });

    myObj.myProperty = false;
    myObj.myProperty;
}());\
> Uncaught TypeError: "myProperty" is read-only

Making effective use of descriptors is a fairly advanced concept, but understanding the internal structure of an object is essential to understanding the syntaxes involved in working with objects in more common ways. For example, these concepts come into play when using the Object.create() static method, which gives you fine-grained control over any prototypes attached to the new object.

Object.create() creates a new object using an existing object as its prototype. This lets the new object inherit properties and methods from another user-defined object the same way that objects inherit properties from JavaScript's built-in Object prototype. When you invoke Object.create() with an object as an argument, it creates an empty object with the passed object as its prototype.

const myCustomPrototype = {
  'myInheritedProp': 10
};

const newObject = Object.create( myCustomPrototype );

newObject;
> Object {  }
<prototype>: Object { myInheritedProp: 10 }
  myInheritedProp: 10
  <prototype>: Object {  }

Object.create can take a second argument specifying own properties for the newly-created object using a syntax similar to Object.defineProperty()— that is, an object mapping keys to a set of descriptor attributes:

const myCustomPrototype = {
  'myInheritedProp': 10
};

const myObj = Object.create( myCustomPrototype, {
        myProperty: {
            value: "The new property value.",
            writable: true,
            configurable: true
        }
  });

myObj;
> Object {  }
    myProperty: "The new property value."
    <prototype>: Object { myInheritedProp: 10 }

In this example, the new object (myObj) uses an object literal (myCustomPrototype) as its prototype, which itself contains the inherited Object.prototype, resulting in a series of inherited prototypes called the prototype chain. Each object has a prototype, whether assigned or inherited, which has an assigned or inherited prototype of its own. This chain ends at a null prototype, which has no prototype of its own.

const myPrototype = {
  'protoProp': 10
};

const newObject = Object.setPrototypeOf( { 'objProp' : true }, myPrototype );

newObject;
> Object { objProp: true }
    objProp: true
    <prototype>: Object { protoProp: 10 }
        protoProp: 10
        <prototype>: Object {  }

The properties contained in a value's prototype are available at the "top level" of an object, without the need to access the prototype property directly:

const objectLiteral = {
    "value" : true
};

objectLiteral;
> Object { value: true }
    value: true
    <prototype>: Object {  }

objectLiteral.toString();
"[object Object]"

This pattern holds true for the entire prototype chain associated with an object: when trying to access a property, the interpreter looks for that property at each "level" of the prototype chain, from top to bottom, until it finds the property or the chain ends:

const myCustomPrototype = {
  'protoProp': "Prototype property value."
};

const myObj = Object.create( myCustomPrototype, {
    myProperty: {
        value: "Top-level property value.",
        writable: true,
        configurable: true
    }
});

myObj.protoProp;
> "Prototype property value."

Check your understanding

Which descriptors are accessors?

[[Set]]
[[Writable]]
[[Get]]