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 tofalse
hides the property from some operations.configurable
: Setting this attribute tofalse
prevents several changes to a property (attributes exceptvalue
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 propertypropertyName
: 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 fals
e (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 iswritable
attribute can be set tofalse
(line 33) if it was originally set totrue
(line 6). Modification of all other descriptor setting will throughTypeError
(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 tofalse
, thenJSON.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, otherwisetrue
. - Object.isSealed(obj): Determines if an object is sealed. Returns
true
if adding/removing properties is prohibited, and all existing properties haveconfigurable: false
. - Object.isFrozen(obj): Determines if an object was frozen. Returns
true
if adding/removing/changing properties is prohibited, and all current properties areconfigurable: 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.