Deep Dive into JavaScript Property Descriptors

Note: This post is work-in-progress learning-note and still in active development and updated regularly.

In a previous learning-post JavaScript Objects – The Basics, we discussed simple key:value object properties (values associated with properties). The value is one of the attribute of a property. Other attributes include enumerable, configurable and writable. These attributes further define an object how its properties can be accessed.

Any property can have zero or more property attributes. Property Attributes, MDN

In a previous learning-post JavaScript Objects – The Basics, use of for..in iteration in an object was  discussed briefly. Enumerable properties show up in for…in loops (once for each property) unless the property’s name is Symbol. In this learning post, we will deep dive into object attributes and enumerability.

Object properties

Object properties contain a key and three or more attributes, (which hold property data).

ECMAScript 6 (ES6) supports following attributes which Dr Rauschmayer groups them into the following three categories:

  • All Properties:
    • enumerable: Setting this attribute to false hides the property from some operations.
    • configurable: Setting this attribute to false prevents several changes to a property (attributes except value can’t be change, property can’t be deleted, etc.).
  • Normal properties (data properties & methods):
    • value: holds the value of the property.
    • writable: controls whether the property’s value can be changed.
  • Accessors (getters/setters):
    • get: holds the getter (a function).
    • set: holds the setter (a function).

These property attributes are not visible when we normally create an object, because they remain hidden and set to true by default. The following table from this post, summarizes how these property descriptor attribute fields are configured by default.

Attributes Data descriptor Accessor descriptor
value Yes No
writable Yes No
enumerable Yes Yes
configurable Yes Yes
get No Yes
set No Yes

Lets look at in the following example and retrieve these property attributes using Object.getOwnPropertyDescriptor() method.

//initialize myCar Object
const myCar = { year: 2017 }

//access myCar object property
Object.getOwnPropertyDescriptor(myCar, 'year')
//OUTPUT
{ value: 2017, 
  writable: true, 
  enumerable: true, 
  configurable: true
}

In the example above, enumberable attribute is set to true. By default, every property is set to true.

Now lets add a new property to myCar object using dot (.) notation.

// add a model property
 myCar.make = "Toyota";

//access property atrutes
Object.getOwnPropertyDescriptor(myCar, 'make')
//OUTPUT
{ value: "Toyota", 
  writable: true, 
  enumerable: true, 
  configurable: true
 }

As shown in the example, enumerable and other attributes are set to true by default.

Defining Property Descriptors

The property attributes can be specified and defined using Object.defineProperty() method. A basic syntax shown below:

//basic syntax
Object.defineProperty(obj, propertyName, descriptor)

Parameters

  • obj : The object on which to define property
  • propertyName : The name of the property to be defined or modified.
  • descriptor : The descriptor of the property being defined or modified.
  • Return value: The object that was passed to the function.

In the example below lets create and define an simple object:

// create and & define an empty object
const myCar = {};
//create object with defineProperty method
Object.defineProperty(myCar, 'make', {
  value: "Toyota",
});
//OUTPUT
{make: "Toyota"}

//Access default attributes
Object.getOwnPropertyDescriptor(myCar, 'make');
//OUTPUT
object {
 value: "Toyota",
 writable: false,
 enumerable: false,
 configurable: false
}

When we compare normally created & defined myCar object earlier, with the myCar object value using Object.defineProperty() method (above, lines: 4-6), all the property attributes of the ‘make‘ are set to false (line ).

That means the make attributes are immutable, not configurable, and not enumerable as shown below:

//re-assign a new value
myCar.make = "Honda"; //=>logs "Honda"

//access myCar.make 
console.log(myCar.make);
//OUTPUT
"Toyota" // writable = false
//delete myCar.make
delete myCar.make; 
//OUTPUT
false //configurable=false
//non-enumerable
Object.keys(myCar); 
//OUTPUT
[] //enumerable = false
How to Validate Property Existence

To better understand how object property descriptors are defined, lets revisit our myCar object below:

// create and & define an empty object
let myCar = { year: undefined };
//add make proprty with defineProperty method
Object.defineProperty(myCar, 'make', { });

//access Property values
console.log(myCar.year); //OUTPUT => undefined
console.log(myCar.make); //OUTPUT => undefined

//access not-existing property value
console.log(myCar.model); //OUTPUT => undefined

In the example above, myCar object was created normal way and year property was assigned with “undefined” value (line 2). Next, a make property was added to myCar object with Object.defineProperty() method without any value (empty). When property values of year and make in myCar object were accessed, both property log undefined output, even for make property with empty value field. Even non-exiting property model outputs a undefined value (line 11).

Because every object descended from myCar inherits hasOwnProperty() method. Using this method an object property can be determined whether an object has a direct property of that object.

//verify with Object.hasOwnProperty()
myCar.hasOwnProperty('year'); //OUTPUT => true
myCar.hasOwnProperty('make'); //OUTPUT => true
myCar.hasOwnProperty('model'); //OUTPUT => false

In the example above, two existing properties (year & make) of myCar object return true (lines: 13-14), where as non-existing property returns false (line: 15).

Modify Exiting Properties with defineProperty

The Object.defineProperty() method allows to create objects and modify or customize properties and their values as shown in the example below:

// Create myCar object
let myCar = {}; //empty object
//add property
Object.defineProperty(myCar, 'make', {
  value: 'Toyota',
  writable: true
});

//modify writable descriptor
Object.defineProperty(myCar, 'make', {
  value: 'Toyota',
  writable: false
});

//access property Descriptor
Object.getOwnPropertyDescriptor(myCar, 'make');
//OUTPUT
Object {
  value: 'Toyota', //unchanged
  writable: false, // modified
  enumerable: false,
  configurable: false
}

In the example above, an empty myCar object was created (line 2). A make property was added to myCar object using Object.defineProperty() method and defined its two attributes value: 'Toyota' and writable: true (lines: 4-7).  Now when we access descriptor attributes of make property using Object.getOwnDescriptor() method (line 10). The output result shows (lines: 11-16) value and writable as defined before (line: 12-13) but other two attributes enumerable and configurable were not defined were set to false by default (lines: 14-15).

Using Object.defineProperty() the descriptor attributes of make property can be modified. In the example above, the writable attribute was changed from true to false (line: 21) and the modification is confirmed in its output (line: 21).

Writable Descriptor Attribute

The writable attribute  is set to false by default when property is defined with Object.definePropert() method and thus the property is non-writable and can’t be reassigned.

// Create myCar object
let myCar = {}; //empty object
//add property
Object.defineProperty(myCar, 'make', {
  value: 'Toyota',
  writable: true //defaults false
});
//access property value
console.log(myCar.make); //=> Toyota

//Change property value
myCar.make = "Honda"; //=> Honda

In the example above, revisiting myCar object its make property is  assigned 'Toyota' value and its writable attribute is defined as true (line 6), default is false. Value of make can be reassigned to 'Honda' (line 12).

//set writable attribute to false
Object.defineProperty(myCar, 'make', {
    writable: false
});
//access property value
console.log(myCar.make); //=> Honda

//change property value
myCar.make = "Toyota"; // logs "Toyota" (TypeError in 'strict mode'
//access property value
console.log(myCar.make); //OUT => Honda

//try modifying writable to true
Object.defineProperty(myCar, 'make', {
    writable: true
});
//OUTPUT
Uncaught TypeError: Cannot redefine property: make

In the example above, the writable attribute of make property is defined as false (line 15). Value of make property can’t be modified (line 21) and throws TypeError.

Once the writable attribute is set to false, it can’t be changed back to true (lines: 26-28) again (the change is permanent – ONE way only) and throws cannot refine property TypeError (line 30).

Tip: any modification in the writable attribute is permanent and ONE way only. Once its set false can’t be changed back to true.

Configurable Descriptor Attribute

As demonstrated in the previous section, when writable attribute is set to false, it prevents from changing property value only. However the property can still be modified.

In the example below, lets revisit & continue from our myCar.make property attribute setting from previous section, add configurable descriptor and set it to true (line 7). The myCar.make property value can still be modified by deleting the myCar.make property (line 13) and reassigning a new value 'Toyota' (line 15).

// Create myCar object
let myCar = {}; //empty object
//add property
Object.defineProperty(myCar, 'make', {
  value: 'Honda',
  writable: false,
  configurable: true
})
//access property value
console.log(myCar.make); // => Honda

//delete property & reassign value
delete myCar.make; //=> true
//modify & reassign property value
myCar.make = 'Toyota'; // logs 'Toyota'

//access modified property value
console.log(myCar.make);  //OUTPUT => Toyota

For example, if we don’t like property values to be constant, and not modifiable then this can be achieved by setting enumerable attribute to true. In the example below, lets examine this property revisiting myCar object and a new model: 'Camry' property value.

// Create myCar object
let myCar = {}; //empty object
//add a model property
Object.defineProperty(myCar, 'model', {
  value: 'Camry',
  writable: true,
  configurable: false
})
//check descriptor attribute setting
Object.getOwnPropertyDescriptor(myCar, 'model');
//OUTPUT
 object {
  value: 'Camry',
  writable: true,
  configurable: false,
  enumerable: false //default setting
})
//modify enumerable setting
"use strict";
Object.defineProperty(myCar, 'model', {
    enumerable: true
});
//OUTPUT
Uncaught TypeError: Cannot redefine property: model

//delete model property
delete myCar.model; //=> false (non-configurable)
//access property value
console.log(myCar.model);//=> Camry
//modify writable setting to false
"use strict";
Object.defineProperty(myCar, 'model', {
    writable: false
});//=> logs "Camry"
//modify property value
myCar.model = 'Corolla'; //=> logs "Corolla"
//access model property value
console.log(myCar.model);//=> Camry (not modified)

In the example above, a model property of myCar object is defined using Object.propertyDefine() method (lines: 4-8) its descriptor attributes are set as value to Camry, writable to true and configurable to false.

By default, its enumerable attribute is set to false (line 16). If we modify the enumerable attribute to true (lines: 18-22), under ‘strict mode‘ its throws out
TypeError: Cannot redefine property (line 24).

Note: Errors are thrown out only in  “strict use” mode. Modification of attribute properties may be ignored in non-strict.

Once the configurable attribute is set to false (line 7), it presents from:

  • Deleting object property (lines: 27-29),
  • Modifying other descriptor attributes (lines: 19-22 & 32-34). One exeption is writable attribute can be set to false (line 33) if it was originally set to true (line 6). Modification of all other descriptor setting will through TypeError (setting the same value does not throw any error).

To make a object property immutable,  both of its  configurable and writable attributes should be set to false.

Tip: Similar to writable attribute, configurable attribute change is ONE way only and can’t be changed back.

Enumerable Attributes

When an object is created, the newly created object inherits certain methods through property inheritance (eg. object.key() method). Most object property are enumerable (where values can be changed) although there is some non-enumerable property too. An enumerable property can be iterated using for..in statement or with object.key() method. An enumerable property can be verified by calling property.enumerable, which return true or false.

In the following example adopted the MDN Documentation, lets examine the enumerable attribute in more detail.

// Create MyCar object
let myCar = {}; //empty object
//add property & assign value 
Object.defineProperty(myCar, 'make', {
  value: 'Toyota',
  enumerable: true
});// logs =>{make: "Toyota"}
//add another property
Object.defineProperty(myCar, 'year', {
  value: 2018,
  enumerable: false
});//logs {make: "Toyota", year: 2018}
// add model property without setting enumerable value
Object.defineProperty(myCar, 'model', {
  value: 'Corolla'
}); // enumerable default to false

//add property with dot notation
myCar.price = '20K'; // enumerable defaults to true

//iterate over myCar with for..in
for (var i in myCar) {
  console.log(i);
}// logs 'make' and 'price'
//OUTPUT
make
price

//access object.key()
Object.keys(myCar); //=>  ['make', 'price']

//access objectIsEnumerable() method
myCar.propertyIsEnumerable('make'); //=> true
myCar.propertyIsEnumerable('year'); //=> false
myCar.propertyIsEnumerable('model'); //=> false
myCar.propertyIsEnumerable('price'); //=> true

In the example above, an empty myCar object is created (line 2) and assign four properties make, year, model and price using object.defineProperty() method (line: 4, 9 & 14) and dot notation method (line 18). For the make property, its enumerable attribute is set as true (line 6) and for year property, it is set as false (line 11). For the model property it is not explicitly defined (line 18) and which defaults to true.

Tip: Property explicitly defined with enumerable as true are only listed with for..in loop or Object.keys() method.

When we iterate over myCar object with for..in loop (lines: 22-24), properties with enumerable attribute set to true (make in line 6, and price by default) are listed (lines: 26-27). Likewise, the same two properties (make & price) are listed (line 30) with Object.keys() method too. Whether a property is enumerable or not could be verified using Object.propertyIsEnumerable() method as shown for myCar object (lines:33-36).

Use Case Example

Enumerable descriptor attribute determines whether object property are listed using for..in loop and Object.keys() method. In practice, enumerable attribute can be modified in the following cases:

  • JSON serialization: JSON is a syntax allows serializing objects, arrays and other data types. Quoting from this post – objects are created based off JSON data retrieved over XHR calls. These objects are then enhanced with a couple of new properties. When POSTing the data back, developers create a new object with extracted properties. If enumerable descriptor is set to false, then JSON.stringify would drop properties from the list, and vice versa.
  • Mixins: Quoting from this post again: Another application could be mixins which add extra behavior to objects. If a mixin has an enumerable getter accessor property; then that calculated property will automatically show up in Object.keys and for..in loops. The getter will behave just like any property.

Revisiting the same myCar object created previously, lets define enumerable & configurable attributes slightly different way and examine its enumerabilty with JSON.stringify() method as shown in the example below:

//create myCar & assign property
let myCar = {
    make: 'Toyota',
    year: 2018
};
//add property with Object.defineProperty()
Object.defineProperty(myCar, 'model', {
  value: 'Corolla',
  enumerable: true,
  configurable: true
}); //logs =>{make: "Toyota", year: 2018, model: "Corolla"}

//assign a car var for Object.keys()
let car = Object.keys(myCar);
//access value on console
console.log(car); //OUTPUT => ["make", "year", "model"]
//iterate keys with for..each
car.forEach(i => console.log(myCar[i]));
//OUTPUT
Toyota
2018
Corolla

//JSON stringify
JSON.stringify(myCar);
//OUTPUT
"{"make":"Toyota","year":2018,"model":"Corolla"}"

//modify enumerable to false
Object.defineProperty(myCar, 'model', {
    enumerable: false
});
//access object.key()
Object.keys(myCar); //=> ["make", "year"]
//JSON stringify
JSON.stringify(myCar);//=>"{"make":"Toyota","year":2018}"

In the example above, myCar object was created and assigned two properties – make and year to it (lines: 2-5). It was  demonstrated earlier that all the descriptor attributes (writable, enumerable and configurable) of such properties are set to true by default.

Another property 'year' was also added to the myCar object with Object.defineProperty() method and assigned configurable and configurable descriptor attributes to true (lines: 9-10). By default, descriptor attributes of properties created this way are set to false by default (eg. writable is set to false by default).

Because the enumerable descriptor is set true, all the three properties of myCar object are listed (lines: 20-22) with forEach iteration (line 18). Likewise, all three properties are listed (line 27) with JSON.stringify() method too (line 25).

Unlike other property descriptors, the enumerable attribute can be modified. When the enumerable descriptor of model property was set to false (line 31) it was dropped from output listings with both Object.keys() method (line 34 )as well as JSON.stringify() method (line 36).

Note: Modification in enumerable attribute is TWO way. Unlike the writable attribute, it can be changed from false to true and vice versa.

The MDN documentation has a list of methods of the object constructor including the following methods for the property descriptors as listed here:

  • Object.preventExtensions(obj): Prevents adding properties to object.
  • Object.seal(obj): Prevents to add/remove properties, sets for all existing properties configurable: false.
  • Object.freeze(obj): It prevents to add/remove/change properties, sets for all existing properties configurable: false, writable: false.
  • Object.isExtensible(obj): Determines if extending of an object is allowed. Returns false if adding properties is prohibited, otherwise true.
  • Object.isSealed(obj): Determines if an object is sealed. Returns true if adding/removing properties is prohibited, and all existing properties have configurable: false.
  • Object.isFrozen(obj): Determines if an object was frozen. Returns true if adding/removing/changing properties is prohibited, and all current properties are configurable: false, writable: false.

Note: The above listed methods are rarely used in practice.

Wrapping Up

In this learning-note, we discussed when objects are created either with object initializers or with static Object.defineProperty() method, their property descriptor attributes are defined differently. Object property descriptors – writable, enumerable, configurable were discussed and how their modification affects property enumeration with for..in loop or Object.keys() method and their use in JSON.stringify() method. Other related but more advance topic Objects Getters and Setters will be discussed separately.

Next Topic: Understanding JavaScript Accessors: Getters & Setters

Useful Resources & Links

While preparing this post, I have referred the following references extensively. Visit original link for additional information.