March 24, 2021

How Object.defineProperty() Works

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.