How Object.defineProperty() Works

At some point, you might have encountered Object.defineProperty()
in someone’s code. It might be through an Angular, React, or Vue project.
However, Object.defineProperty()
isn’t actually specific to any of the listed framework/library. It’s actually part of vanilla JavaScript.
Vanilla JavaScript is the foundation of every JavaScript-based library, project, module, and widget. It is just JavaScript in its purest form — unfiltered, no-prewritten configurations and structural requirements. When we work in Angular, React or Vue, we are working with JavaScript.
As we move towards frameworks and libraries, many devs are missing out on the finer details of the scripting language. Which is why Object.defineProperty()
is often a mystery for many developers.
In this piece, we’re going to uncover Object.defineProperty()
, dig into its secrets and see how it can benefit the way we write our JavaScript code.
Objects in a nutshell
In JavaScript, an object looks something like this:
let someObj = {
name: 'Tibbers'
}
Here’s what all the different parts of an object is called:

Each property in an object has a thing called descriptors
. A descriptor
lets you configure what can happen to that property.
So rather than just setting up a straightforward object, you can configure its accessibility, how values are set, its immutability, and if it is enumerable.
There are two descriptor groups that you use for defining your properties. It’s also good to note that when you define the descriptors, you can only use one of the descriptor groups and not both.
So if you use the DATA side, you can’t configure the ACCESSOR side.
Here’s the table:
DATA ACCESSOR DEFAULT SETTINGS
--------------------------------------------------------------
value get if no value, undefined
writable set true
configurable configurable true
enumerable enumerable true
Defining properties
Under normal default circumstances, you can just create an object by doing something like this:
let cart = {
cartId: 2981,
shipping: '123 Somewhere street',
items: [
{sku: 2341, name: 'Apples', quantity: 1, unit: 'kg'},
{sku: 2213, name: 'ham', quantity: 100, unit: 'grams'}
]
}
And when you loop through it to get the properties, everything shows up.
for(let prop in cart){
console.log(prop);
}
//will log out "cartId", "shipping" and "items"
However, what if you don’t want cartId
to be accessible through a loop. What if you want to protect it against change? The last thing you need is some side effect that accidentally messes with your cartId
.
Rather than creating it through the normal method of assignment, you can do it using Object.defineProperty()
.
Object.defineProperty()
takes in three values: the object you want to target, the property name, and the property settings.
In short, it looks something like this:
Object.defineProperty(objNameHere, propNameHere, {});
So, basically, either the DATA
column or the ACCESSOR
column.
DATA ACCESSOR
---------------------------
value get
writable set
configurable configurable
enumerable enumerable
Let’s take a look at the DATA column first.
DATA
------------
value
writable
configurable
enumerable
To create an immutable cartId
property inside the cart
object, we can set the writable
value to false
. configurable
deals with your ability to delete it from the object and if property descriptors can be changed further down the line. enumerable
determines if the property will show up during an enumeration process.
In order to make our cartId
not rewritable, prevent it from accidental deletion and keep it out of loops, you can create it using the following:
Object.defineProperty(cart, 'cartId', {
value: 2981,
writable: false,
configurable: false,
enumerable: false
})
So now if you run the following loop, cartId
won’t show up. However, if you get specific with console.log
, it will show. If you try to change or delete the value, it will silently fail.
Why?
Because configurable
and enumerable
is set to false
.
let log = console.log;
let cart = {
shipping: '123 Somewhere street',
items: [
{sku: 2341, name: 'Apples', quantity: 1, unit: 'kg'},
{sku: 2213, name: 'ham', quantity: 100, unit: 'grams'}
]
}
Object.defineProperty(cart, 'cartId', {
value: 2981,
writable: false,
configurable: false,
enumerable: false
})
cart.cartId = 9999;
delete cart.cartId;
//both won't work and silently fails
for(let prop in cart){
log(prop);
}
// will log out "shipping" and "items"
log(cart.cartId)
// will log out 2981
So that’s Object.defineProperty()
in a nutshell for the DATA
column. But what about the ACCESSOR
side?
The ACCESSOR
is what you can do to the value itself. In short, we’re dealing with the methods that make the property values what they are.
Here’s the ACCESSOR
table again.
ACCESSOR
-----------
get
set
configurable
enumerable
Let’s pretend we want to do something special to the value when it is set
on a property. In addition to this, we still want it to remain unwritable and keep it out of the enumeration process.
To do this, you can write something like this:
Object.defineProperty(cart, 'cartId', {
configurable: true,
enumerable: false,
get () => this.value,
set: (_val) => { this.value = 'ID' + _val; }
})
This will result in the following output when cartId
gets set.
cart.cartId = 9812;
//will create ID9812
However, this won’t stop it from getting accidentally deleted in the future. To prevent this, you can redefine parts of the property like this:
Object.defineProperty(cart, 'cartId', {
configurable: false,
})
This will prevent any future changes down the line, including if anyone tries to flip the configurable
status to true
in the future. Doing so will result in a TypeError
.
That’s basically it for the scope of this piece.
I hope that it all makes sense and sparks some ideas on how to use it for your next Angular, React, Vue, or any JavaScript based project.
Comments ()