Overview
Objects, as we know, contain properties. Each of the properties of an object has three configuration flags, which can take the values true
or false
, in addition to the value. These flags are called descriptors:
-
writable
- whether the property is writable; -
enumerable
- whether the property is visible during enumerations (e.g. in afor..in
loop); -
configurable
- whether the property is available for reconfiguration.
When we create an object property in the "normal way", these three flags are set to true
.
The static method Object.defineProperty()
is used to change descriptor values, and Object.getOwnPropertyDescriptors()
is used to read values.
In other words, descriptors are key-value pairs that describe the behaviour of an object property when performing operations on it (e.g., reading or writing).
There is a set of descriptors attached to each property of the object under the bonnet.
Example
Let's create an object and add the OS property for the laptop to it. Let's do it using descriptors and static method Object.defineProperty()
.
Passing it to the method:
- Object to which we add the property;
- Property name with a string;
- Object with descriptor values and a
value
key containing the property value.
const laptop = {}
Object.defineProperty(laptop, 'os', {
value: 'MacOS',
writable: false,
enumerable: true,
configurable: true
})
The os
property will not be available for overwriting, but will be visible when enumerated and available for reconfiguration.
Let's try to overwrite the os property and display the result:
laptop.os = 'Windows'
console.log(laptop) // { 'os': 'MacOS' }
How to Spell
Object.defineProperty(object, property, descriptors)
The function accepts the following parameters:
-
object
- object whose property we are modifying or adding; -
property
- the property for which you want to apply the descriptor; -
descriptors
- a descriptor describing the behaviour of the property.
If the property already exists, Object.defineProperty()
will update the flags.
If the property does not exist, the method creates a new property with the specified value and flags. If any flag is not explicitly specified, it is assigned the value false
.
How to Realise
The descriptors we can pass to Object.defineProperty()
come in two types - data descriptor and access descriptor. Each type of descriptor has its own set of properties.
In both types, the common properties configurable
and enumerable
can be used.
The descriptor passed to Object.defineProperty()
can only be one type of descriptor. It cannot be both at the same time! If you pass to Object.defineProperty()
an object containing both data descriptor and access descriptor properties, the method will throw the Invalid property descriptor error. Cannot both specify accessors and a value or writable attribute.
Data Descriptor
A data descriptor is a descriptor that defines the value of a property and the ability to change that value.
-
value
- property value, defaults toundefined
. -
writable
- whether the value can be changed using an assignment operator.
value
The value
property of the data descriptor is responsible for the value of the object property.
Let's add a "Screen Size" property to the notebook:
Object.defineProperty(laptop, 'displaySize', {
value: '17'
})
Let's derive the obtained data:
const descriptor = Object.getOwnPropertyDescriptor(laptop, 'displaySize')
console.log(descriptor)
We did not specify the other properties explicitly, so the descriptor has the following values:
{
"value": "17",
"writable": false,
"enumerable": false,
"configurable": false
}
writable
The writable
property of the descriptor determines whether the value of the property can be changed using an assignment operator. It is set to false
by default for properties created via Object.defineProperty()
and to true
if the property is added via .
.
Let's change the value of writable
:
const laptop = {}
Object.defineProperty(laptop, 'displaySize', {
value: '17',
writable: false, // Not rewritable.
configurable: true,
enumerable: true
})
laptop.displaySize = '18'
console.log(laptop.displaySize) // { 'displaySize': '17' }
In strict mode, we'll get a TypeError
, which says we can't change a non-overwritable property.
Access Descriptor
An access descriptor is a descriptor that defines the operation of a property through the property's read and write functions (getter and setter).
get
- function used to get the value of a property, returns a value or undefined
.
set
- a function used to set the value of the property. It takes as its only argument the new value assigned to the property.
🧐 You can see that the two types of descriptors conflict with each other. They describe the same thing - the operation of a property, but they do it in different ways. That's why you can't use a descriptor with properties of both types.
Let's compare a simple object with a name
field and an object with a name
getter created via Object.defineProperty()
:
const animal = { _hiddenName : 'Cat' }
Object.defineProperty(animal, 'name', {
get: function() { return this._hiddenName }
})
const animal2 = {
name: "And there's a cat in here, too",
}
console.log(animal.name) // Cat
console.log(animal2.name) // And there's a cat in here, too
Both objects have the same behaviour. It is only worth mentioning that the property in the first case is followed by a function that is called automatically. It is enough to write animal.name
.
If we need to change the value of the name
property, we do animal.name = 'Grey Cat'
, nothing will happen. The reason is that there is no setter function associated with the name
key, so it is not possible to set a value for this property.
Add a setter:
const animal = { _hiddenName : 'Cat' }
Object.defineProperty(animal, 'name', {
get: function() { return this._hiddenName },
set: function(value){ this._hiddenName = value }
})
animal.name = 'Dog'
console.log(animal.name) // Dog
Essentially, we can regulate the ability to read and retrieve the value of a property, just as we can in a data descriptor, only more subtly. This approach is used frequently, so we came up with a syntax for declaring getters and setters without calling Object.defineProperty()
:
const animal = {
get name() {
return this._name
},
set name(value) {
this._name = value
}
}
console.log(animal.name) // undefined
animal.name = 'Cat'
console.log(animal.name) // Cat
Setters may be needed, for example, to modify a value when writing properties. In the example below, we modify the date and write it in the desired format.
const updatedAt = {
get date() {
return this._date
},
set date(value) {
this._date = new Intl.DateTimeFormat('en-US').format(value)
}
}
Let's write the date and time in the date
field:
updatedAt.date = new Date(2025, 11, 12)
console.log(updatedAt.date) // 12/12/2025
And we get the date in the correct format: 12/12/2025
.
Properties with access methods give us all the data processing capabilities of functions and the simplicity characteristic of working with ordinary properties.
General Properties
Common properties can be specified in both types of descriptors.
enumerable
Defines whether the object property to be created is visible in enumerations.
Let's create two properties on the laptop
object - one will be enumerated and one will not:
const laptop = {}
Object.defineProperty(laptop, 'processor',
// Let's make the processor enumerable as usual:
{ enumerable: true, value: 'Intel Core' }
)
Object.defineProperty(laptop, 'touchID',
// Make `touchID` NOT enumerated:
{ enumerable: false, value: true }
)
console.log(laptop.touchID) // true
console.log(('touchID' in laptop)) // true
console.log(laptop.hasOwnProperty('touchID')) // true
for (let key in laptop) {
console.log(key, laptop[key])
} // 'processor': 'Intel Core'
Notice that laptop.touchID
exists and has a value, but does not appear in the for..in
loop (yet, it exists if you use the in
operator). "Enumerable" means: "will be counted by going through the object's properties by enumerating"."
configurable
The configurable
property determines whether the object property being created is available for reconfiguration.
Let's change the configurable
value:
const laptop = {}
Object.defineProperty(laptop, 'processor', {
value: 'Intel Core',
writable: true,
configurable: false, // Prohibit reconfiguration.
enumerable: true
})
console.log(laptop.processor) // Intel Core
laptop.processor = 'M1'
console.log(laptop.processor) // 'M1'
Object.defineProperty(laptop, 'processor', {
value: 'M1 TOP',
writable: true,
configurable: true,
enumerable: true
}) // TypeError: Cannot redefine property: processor.
Attempting to rewrite the processor
property descriptor results in a TypeError
, even if you are not in strict mode.
☝️ Be careful, changing configurable to false is irreversible and cannot be undone.
If the property is already configurable: false
, writable
can be changed from true
to false
without error, but not back to true
if it is already false
.
And also configurable: false
prevents you from being able to use the delete
operator to delete an existing property. No error will occur, but the property will not be deleted:
delete laptop.processor
console.log(laptop) // { processor: 'M1' }
From time to time, a developer needs to protect objects from tampering. It is easy to change an object property by mistake. To protect objects from such changes and manage their immunity, we propose to use descriptors such as writable
and configurable
, setters, and the methods Object.preventExtensions()
, Object.seal()
, and Object.freeze()
to restrict access to the entire object.
Support ❤️
It took a lot of time and effort to create this material. If you found this article useful or interesting, please support my work with a small donation. It will help me to continue sharing my knowledge and ideas.
Make a contribution or Subscription to the author's content: Buy me a Coffee, Patreon, PayPal.