DEV Community

Samuel Ochaba
Samuel Ochaba

Posted on

The Secret Life of JavaScript Objects: Flags and Descriptors

Most of us learn JavaScript objects like this:

const user = {
  name: "John",
  age: 30
};
Enter fullscreen mode Exit fullscreen mode

It looks simple. A key, a value. Done.

But what if I told you that every property in an object has hidden settings? What if you wanted to create a property that cannot be deleted? Or a property that is invisible to loops? Or a value that is read-only, like Math.PI?

Welcome to the world of Property Flags and Descriptors. This is how you take off the training wheels and gain full control over your objects.

What are Property Flags?

Imagine a file on your computer. It has content, but it also has attributes: Read-only, Hidden, or System file.

JavaScript object properties work the same way. Besides the value, every property has three special "flags" (attributes) that are usually set to true by default:

  1. writable: If true, the value can be changed. If false, it's read-only.
  2. enumerable: If true, it shows up in loops (like for..in). If false, it's invisible.
  3. configurable: If true, the property can be deleted or modified. If false, it’s locked down forever.

How to View the Hidden Flags

To see these secret settings, we use Object.getOwnPropertyDescriptor.

let user = {
  name: "John"
};

let descriptor = Object.getOwnPropertyDescriptor(user, 'name');

console.log(descriptor);
/* Output:
{
  "value": "John",
  "writable": true,
  "enumerable": true,
  "configurable": true
}
*/
Enter fullscreen mode Exit fullscreen mode

See? When you create an object normally, JavaScript is nice and sets everything to true for you.

Changing the Flags: The defineProperty Method

To change these settings, we use Object.defineProperty.

** The Trap for Beginners:**
If you use defineProperty to modify an existing property, it only changes what you tell it to.
BUT, if you use it to create a new property, any flag you don't mention defaults to false.

Let's look at the flags in action with real-world scenarios.

1. writable: false (The "Read-Only" ID)

Imagine you are building a User object. You want users to be able to change their name, but never their generic id.

let user = {
  name: "John",
  id: 12345
};

Object.defineProperty(user, "id", {
  writable: false // Make it read-only
});

// Let's try to hack the ID
user.id = 99999; 

console.log(user.id); // 12345 (The change was ignored!)
Enter fullscreen mode Exit fullscreen mode

Note: In "Strict Mode" ("use strict"), trying to write to a non-writable property will throw a loud Error. In non-strict mode, it just fails silently.

2. enumerable: false (The "Invisible" Logic)

Sometimes you want to add helper methods to an object, but you don't want them cluttering up your data when you loop through it.

Let's say we have a custom toString method.

let user = {
  name: "John",
  toString() {
    return this.name;
  }
};

// By default, the loop sees everything
for (let key in user) console.log(key); 
// Output: name, toString. We don't want this here!

// Let's hide it
Object.defineProperty(user, "toString", {
  enumerable: false
});

// Now try again
for (let key in user) console.log(key); 
// Output: name (Clean! )
Enter fullscreen mode Exit fullscreen mode

This is how built-in methods works. You don't see the standard .toString() when you loop over objects because it is set to enumerable: false.

3. configurable: false (The "Nuclear" Option)

This is a one-way road. If you set configurable: false, you generally cannot change the property flags anymore, and you cannot delete the property.

Think of Math.PI. It would be catastrophic if a developer accidentally deleted Pi or changed it to 3.

let myMath = {};

Object.defineProperty(myMath, "PI", {
  value: 3.14159,
  writable: false,
  configurable: false // 🔒 Sealed shut
});

delete myMath.PI; // returns false
console.log(myMath.PI); // 3.14159 (Still there!)

// Trying to re-configure it will throw an error:
// Object.defineProperty(myMath, "PI", { writable: true }); TypeError
Enter fullscreen mode Exit fullscreen mode

Advanced: The "Perfect" Clone

A common interview question is: How do you clone an object?
Most people suggest const clone = { ...original } or a for..in loop.

The Problem: Those methods copy the values, but they lose the flags. If you clone an object that has a read-only property using the spread operator, the clone's property will be writable.

To clone an object exactly (including flags, getters, and setters), use Object.getOwnPropertyDescriptors:

let clone = Object.defineProperties({}, Object.getOwnPropertyDescriptors(user));
Enter fullscreen mode Exit fullscreen mode

This copies not just the data, but the "soul" (metadata) of the object.

Global Object Sealing

If defining properties one by one feels tedious, JavaScript offers three levels of security for the entire object:

  1. Object.preventExtensions(obj): No new properties can be added.
  2. Object.seal(obj): No adding/removing properties. Existing ones are non-configurable.
  3. Object.freeze(obj): The ultimate lockdown. No adding, removing, or changing values. The object is a constant.

Summary

  • Objects are more than key-values: They contain metadata called "descriptors".
  • writable: Controls if you can change the value.
  • enumerable: Controls if the property shows up in loops.
  • configurable: Controls if you can delete or reconfigure the property.
  • Use Object.defineProperty to set these flags and write more robust, professional code.

Top comments (0)