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?
[[Get]]
[[Set]]
[[Writable]]