Mastering JavaScript Object.defineProperty: A Comprehensive Guide with Coding Examples

Amresh Kumar
8 min readJan 11, 2024

--

Introduction:

In the dynamic world of JavaScript, the ability to precisely control object properties is a crucial skill for developers. The Object.defineProperty() method provides a powerful mechanism to define or modify properties on objects with fine-grained control. In this article, we will delve deep into the intricacies of Object.defineProperty(), exploring property flags, property descriptors, and practical coding examples.

Understanding Property Flags and Property Descriptor:

Before diving into Object.defineProperty(), it's essential to grasp the concepts of property flags and property descriptors. Property flags, such as configurable, enumerable, writable, get, set, and value, determine the characteristics of a property. The property descriptor, accessible through Object.getOwnPropertyDescriptor(), encapsulates these flags along with additional information.

const obj = { key: 'value' };
const descriptor = Object.getOwnPropertyDescriptor(obj, 'key');
console.log(descriptor);
//{value: 'value', writable: true, enumerable: true, configurable: true}

What is object defineProperty () used for?

Object.defineProperty() allows developers to add or modify properties on objects. Unlike standard property assignment, this method enables the customization of property attributes.

Syntax

Object.defineProperty(obj, key, descriptor);

Parameters

  • obj (object) is the object you're working with, the place where you want to make changes.
  • key (string) is the name or title of the property you want to create or modify. It's like the label on a box.
  • descriptor (propertyDescriptorObject) is the property descriptor that holds the various property flags we discussed earlier.

Return Value

The object that was passed to the function Object.defineProperty() for modification.

So, the whole thing is like saying, “Hey JavaScript, in this object (obj), let's do something to this property (key), and here are the rules (descriptor) it should follow".

It’s important to note that the descriptor is optional, meaning you don’t have to provide it if you want to use the default settings for the property

By default, properties added using Object.defineProperty() are not writable, not enumerable, and not configurable.

Below examples illustrate the Object.defineProperty() method in JavaScript:

const obj = {};

Object.defineProperty(obj, 'newKey', {
value: 'initialValue',
});

console.log(Object.getOwnPropertyDescriptor(obj, 'newKey'));
// Outputs: {writable:false, configurable:false, enumerable:false}

Using Object.defineProperty() for Precise Property Management

You have the option to pass a descriptor object containing property flags like value, writable, configurable, enumerable, get, and set. These flags allow you to specify precise behaviors when accessing or modifying a key.

const obj = {};

// Adding a new property with specific attributes
Object.defineProperty(obj, 'newKey', {
value: 'initialValue',
writable: true,
enumerable: true,
configurable: true
});

console.log(obj.newKey); // initialValue

In this example, a new property named ‘newKey’ is added to the object ‘obj’ with an initial value of ‘initialValue’. The property is set to be writable, enumerable, and configurable, and when accessed, it logs ‘initialValue’ to the console.

Understanding Property Flags:

Value

The value property flag is used to assign a specific value to the property being defined or modified. This flag is applicable to data descriptors, and it represents the initial value associated with the property.

const obj = {};
Object.defineProperty(obj, 'exampleValue', {
value: 'initialValue'
});
console.log(obj.exampleValue); // Output: initialValue

In this example, the property ‘exampleValue’ is defined with the value flag set to 'initialValue'.

Writable

The writable property flag determines whether the value of the property can be changed using an assignment operator. If set to false, attempts to modify the property's value will result in an error.

const obj = {};
Object.defineProperty(obj, 'readOnlyProperty', {
value: 'immutableValue',
writable: false
});
obj.readOnlyProperty = 'newValue';
console.log(obj.readOnlyProperty); //immutableValue

Here, ‘readOnlyProperty’ is defined as non-writable, preventing any assignment to its value after the initial definition.

Enumerable

The enumerable property flag controls whether the property will be included during enumeration of the object's properties. If set to false, the property will be skipped in operations like for...in and Object.keys().

const obj = {
visibleProperty: 'visibleValue',
hiddenProperty: 'hiddenValue',
};

Object.defineProperty(obj, 'hiddenProperty', {
enumerable: false,
});

// Enumerate only enumerable properties
console.log(Object.keys(obj)); // Output: ['visibleProperty']

// looping over object
for(let key in obj){
console.log(key); // visibleProperty
}

// Check if property is directly on the object
console.log(obj.hasOwnProperty('visibleProperty')); // Output: true
console.log(obj.hasOwnProperty('hiddenProperty')); // Output: true, despite being non-enumerable

// Check if property exists (including in prototype chain)
console.log('visibleProperty' in obj); // Output: true
console.log('hiddenProperty' in obj); // Output: true, despite being non-enumerable

In this case, ‘hiddenProperty’ is modified as non-enumerable, making it hidden during property enumeration.

Object.prototype.propertyIsEnumerable

The propertyIsEnumerable function is a method in JavaScript that is used to check whether a specified property of an object is enumerable. It is called on an object and takes a property name as an argument. If the specified property exists on the object and is enumerable, the function returns true; otherwise, it returns false. This method is particularly useful when you want to determine if a property can be iterated over in a loop.

Here’s a simple example:

const myObject = {
name: 'Amk',
age: 25,
};

console.log(myObject.propertyIsEnumerable('name')); // true
console.log(myObject.propertyIsEnumerable('age')); // true
console.log(myObject.propertyIsEnumerable('gender')); // false (non-existent property)

In this example, propertyIsEnumerable is used to check the enumerability of the 'name' and 'age' properties in the myObject. The method returns true for existing enumerable properties and false for non-existent or non-enumerable properties.

Get and Set

The get and set property flags are used to define accessor properties, allowing custom getter and setter functions for a property.

const obj = {
_value: 0
};
Object.defineProperty(obj, 'value', {
get(){
return this._value;
},
set(newValue) {
if (newValue >= 0) {
this._value = newValue;
} else {
console.error('Invalid value. Please provide a non-negative number.');
}
},
enumerable:false
});
obj.value = 42;
console.log(obj); // Output: { _value: 42 }

obj.value = -1; // Output: Invalid value. Please provide a non-negative number.

In this example,custom getter and setter functions are defined as accessor properties, allowing controlled access and modification of the property’s value.

Configurable

The configurable property flag in JavaScript plays a crucial role in determining the mutability of a property. This flag controls whether property can be deleted from the object and whether and whether we’re allowed to tweak its attributes (except for value and writable).

const obj = {};

// Set configurable to false, making the property non-deletable
// Set enumerable to true, allowing the property to show up during enumeration
Object.defineProperty(obj, 'property', {
value: 'value',
configurable: false,
enumerable: true
});

console.log(obj); // { property: 'value' }

// Attempt to delete the 'property' from the object
// Since configurable is set to false, deletion will not succeed
delete obj.property;

console.log(obj); // { property: 'value' }

// adding another property
Object.defineProperty(obj, 'exampleProperty', {
value: 'initialValue',
configurable: true,
writable: true,
enumerable: true,
});

// Modify the configurable flag to false (allowed)
Object.defineProperty(obj, 'exampleProperty', {
writable: false,
enumerable: false,
});

// Attempt to modify the configurable flag (not allowed, will throw an error)
Object.defineProperty(obj, 'exampleProperty', {
writable: true,
enumerable: true,
}); // throws error

The code defines a non-deletable and enumerable property named ‘property’ with the value ‘value’ within an object, demonstrating the impact of the configurable: false flag on the deletion attempt.
We initially define a property with various flags. We then demonstrate that the configurable, writable, and enumerable flags can be modified, but attempting to modify the configurable flag again after it has been set to false will result in an error.

Understanding Data Descriptors vs. Accessor Descriptors in JavaScript

In JavaScript, properties within an object can be broadly categorized into two types: data descriptors and accessor descriptors. These descriptors define how properties behave, influencing aspects such as assignment, retrieval, and enumeration. Let’s explore each type with coding examples to illustrate their characteristics.

Data Descriptors:

Data descriptors deal with properties that store values. These descriptors define the property’s value and its mutability through flags like writable, enumerable, and configurable.

// Creating an object with a data descriptor
const dataDescriptorObj = {};
Object.defineProperty(dataDescriptorObj, 'dataProperty', {
value: 'initialValue', // Set the initial value
writable: true, // Allow value modification
enumerable: true, // Allow enumeration
configurable: true // Allow configuration changes
});

console.log(dataDescriptorObj.dataProperty); // Output: initialValue

// Attempt to modify the value
dataDescriptorObj.dataProperty = 'newValue';
console.log(dataDescriptorObj.dataProperty); // Output: newValue

In this example, dataProperty is a data descriptor that allows both reading and writing, making it mutable.

Accessor Descriptors:

On the other hand, accessor descriptors involve properties with get and set functions, allowing for customized behavior during property retrieval and assignment.

// Creating an object with an accessor descriptor
const accessorDescriptorObj = {
_value: 0, // Private variable to store the value
};

Object.defineProperty(accessorDescriptorObj, 'accessorProperty',{
get() {
return this._value; // Custom behavior during retrieval
},
set(newValue) {
this._value = newValue; // Custom behavior during assignment
},
})

console.log(accessorDescriptorObj.accessorProperty); // Output: 0

// Modifying the value through the set function
accessorDescriptorObj.accessorProperty = 42;
console.log(accessorDescriptorObj.accessorProperty); // Output: 42

Here, accessorProperty is an accessor descriptor with a custom get function for retrieval and a set function for assignment. This allows for controlled behavior during property access.

Cannot both specify accessors and a value or writable attribute, Javascript Error:

JavaScript provides two main approaches for defining properties using Object.defineProperty(): data descriptors, with value and potentially writable, and accessor descriptors, with get and/or set functions. The error occurs when there's an attempt to mix these approaches for the same property.

const obj = {};

// Attempt to define a property with both a direct value and a getter function
Object.defineProperty(obj, 'conflictingProperty', {
value: 'directValue',
get: function () {
return this._computedValue;
},
});

// Throws the error: "Cannot both specify accessors and a value or writable attribute, #<Object>"

To resolve this issue, developers must decide whether the property should have a direct value (using value or writable) or if custom behavior is desired (using get or set). The choice depends on the intended use case for the property.

Object.defineProperties() Method:

The Object.defineProperties() method serves as a powerhouse, allowing us to add or modify multiple properties in a single stroke. By taking an object as its first argument and an object containing property names and their respective descriptors as key-value pairs, this method elevates our property management game. In the provided example, the multiplePropertiesObject showcases the ease with which multiple properties can be defined, providing a clean and efficient alternative.

const multiplePropertiesObject = {};
Object.defineProperties(multiplePropertiesObject, {
age: {
value: 25,
writable: true,
enumerable: true,
configurable: true,
},
city: {
value: "Patna",
writable: true,
enumerable: true,
configurable: true,
},
});
console.log(multiplePropertiesObject); // {age:25, city:Patna}

Object.getOwnPropertyDescriptors() Method:

Another gem in the JavaScript toolkit is the Object.getOwnPropertyDescriptors() method. This method takes an object and returns a comprehensive list of all its property descriptors. The output includes details such as configurability, enumerability, value, and writability. In the illustrated example, the descriptors for properties x and y within the object ob are revealed, offering a holistic view of their attributes.

const multiplePropertiesObject = {};
Object.defineProperties(multiplePropertiesObject, {
age: {
value: 25,
writable: true,
enumerable: true,
configurable: true,
},
city: {
value: "Patna",
writable: true,
enumerable: true,
configurable: true,
},
});
console.log(Object.getOwnPropertyDescriptors(multiplePropertiesObject));
/*
{
age: {value: 25, writable: true, enumerable: true, configurable: true},
city: {value: 'Patna', writable: true, enumerable: true, configurable: true}
}
*/

Enhancing Default Values with Object.defineProperty():

When it comes to adding properties and setting default values in JavaScript objects, it’s crucial to grasp the differences between the seemingly straightforward dot notation and the more nuanced Object.defineProperty(). Let's explore this with a practical example:

const myObject = {};

// Using dot notation for property assignment
myObject.age = 25;

// Equivalent Object.defineProperty() with default attributes
Object.defineProperty(myObject, 'age', {
value: 25,
writable: true,
configurable: true,
enumerable: true
});

// In contrast, the following...
Object.defineProperty(myObject, 'score', { value: 100 });
// is equivalent to:
Object.defineProperty(myObject, 'score', {
value: 100,
writable: false,
configurable: false,
enumerable: false
});

Refrences:

--

--

Amresh Kumar

Software Engineer with 3 years of experience in building full-stack application.