Property flags and descriptors
You already know objects hold properties, and up to now a property has looked like a plain key-value pair: user.name is "Maya", end of story. That picture is incomplete. Behind each property sits a small block of configuration that controls whether the value can change, whether loops can see it, and whether the property can be reconfigured or deleted at all.
This lesson pulls back that curtain. Once you can read and set those hidden controls, you can build read-only constants, hide helper methods from for..in, and freeze whole objects so nothing about them can shift. The next chapter builds on the same machinery to turn a property into a pair of getter/setter functions.
Property flags
A regular data property stores a value, and alongside it three boolean attributes — the course calls them “flags”:
writable— whentrue, the value can be reassigned. Whenfalse, the property is read-only.enumerable— whentrue, the property shows up infor..inloops andObject.keys. Whenfalse, it stays out of them.configurable— whentrue, the property can be deleted and its flags can be edited. Whenfalse, that door locks.
You haven’t run into these because they normally stay invisible. Create a property the ordinary way — user.name = "Maya" or a literal { name: "Maya" } — and all three flags come out true. They’re there; they just don’t advertise themselves. And you can change any of them whenever you like.
Start by reading the flags. The method Object.getOwnPropertyDescriptor hands you the full record for a single property.
let descriptor = Object.getOwnPropertyDescriptor(obj, propertyName);
It takes two arguments: obj, the object you’re inspecting, and propertyName, the key you want to know about. What comes back is a property descriptor — a plain object holding the value together with every flag.
let user = {
name: "Maya"
};
let descriptor = Object.getOwnPropertyDescriptor(user, 'name');
alert( JSON.stringify(descriptor, null, 2 ) );
/* property descriptor:
{
"value": "Maya",
"writable": true,
"enumerable": true,
"configurable": true
}
*/
To write flags instead of reading them, reach for Object.defineProperty.
Object.defineProperty(obj, propertyName, descriptor)
You pass the target obj, the propertyName you’re defining, and a descriptor object describing the value and flags to apply. The behavior splits two ways:
- If the property already exists,
definePropertyupdates only the flags you list and leaves the rest alone. - If the property doesn’t exist yet, it creates one. And here’s the catch that trips people up: any flag you leave out defaults to
false, nottrue.
That default is the opposite of what the literal syntax does. Watch what happens when you create name through defineProperty and mention only the value:
let user = {};
Object.defineProperty(user, "name", {
value: "Maya"
});
let descriptor = Object.getOwnPropertyDescriptor(user, 'name');
alert( JSON.stringify(descriptor, null, 2 ) );
/*
{
"value": "Maya",
"writable": false,
"enumerable": false,
"configurable": false
}
*/
Compare that to the user.name from a literal earlier, where every flag was true. Here all three came out false because you didn’t ask for them. When you build a property with defineProperty and want it to behave like a normal one, spell out writable: true, enumerable: true, configurable: true yourself.
enumerable: true
configurable: true
enumerable: false
configurable: false
With reading and writing covered, walk through what each flag actually does.
Non-writable
Flip writable to false and the property becomes read-only — reassignment stops working.
let user = {
name: "Maya"
};
Object.defineProperty(user, "name", {
writable: false
});
user.name = "Raj"; // Error: Cannot assign to read only property 'name'
From now on nobody can rename this user — unless they run their own defineProperty to reverse the flag, which the still-true configurable flag permits here.
You can build the read-only property from scratch too. Just remember the false-by-default rule and switch on whatever else you need:
let user = { };
Object.defineProperty(user, "name", {
value: "Maya",
// for new properties we need to explicitly list what's true
enumerable: true,
configurable: true
});
alert(user.name); // Maya
user.name = "Raj"; // Error
Non-enumerable
Add a method of your own to an object and it shows up in loops by default. The engine’s built-in toString doesn’t — it’s created non-enumerable, so for..in skips it. Your custom toString gets no such treatment:
let user = {
name: "Maya",
toString() {
return this.name;
}
};
// By default, both our properties are listed:
for (let key in user) alert(key); // name, toString
If you’d rather keep toString out of the loop — it’s plumbing, not data — set enumerable: false and it drops out, matching how the built-in behaves:
let user = {
name: "Maya",
toString() {
return this.name;
}
};
Object.defineProperty(user, "toString", {
enumerable: false
});
// Now our toString disappears:
for (let key in user) alert(key); // name
The same flag governs Object.keys, so it also drops the hidden property:
alert(Object.keys(user)); // name
Try it live. Toggle the enumerable flag on toString, then run a for..in — the key appears and disappears while user.toString() keeps working regardless.
Non-configurable
The configurable: false flag is the strict one. Some built-in objects ship with it preset. Once a property is non-configurable, you can’t delete it and you can’t edit its attributes — the flags freeze in place.
Math.PI is a textbook example. It’s non-writable, non-enumerable, and non-configurable all at once:
let descriptor = Object.getOwnPropertyDescriptor(Math, 'PI');
alert( JSON.stringify(descriptor, null, 2 ) );
/*
{
"value": 3.141592653589793,
"writable": false,
"enumerable": false,
"configurable": false
}
*/
So there’s no way to reassign or redefine Math.PI in your code:
Math.PI = 3; // Error, because it has writable: false
// delete Math.PI won't work either
And because it’s locked, you can’t even talk it into becoming writable again:
// Error, because of configurable: false
Object.defineProperty(Math, "PI", { writable: true });
Math.PI is untouchable — value, flags, existence, all sealed.
The key thing to internalize: making a property non-configurable is a one-way trip. defineProperty can’t undo it. There is no configurable: true that brings the flexibility back.
Here user.name is non-configurable but stays writable, so the value still moves while deletion fails:
let user = {
name: "Maya"
};
Object.defineProperty(user, "name", {
configurable: false
});
user.name = "Raj"; // works fine
delete user.name; // Error
Combine both flags and you get a permanent constant, the same shape as Math.PI:
let user = {
name: "Maya"
};
Object.defineProperty(user, "name", {
writable: false,
configurable: false
});
// won't be able to change user.name or its flags
// all this won't work:
user.name = "Raj";
delete user.name;
Object.defineProperty(user, "name", { value: "Raj" });
Put all three flags under your fingers at once. Pick a combination, define user.name, then try to reassign it, delete it, or list the keys — the outcomes below are exactly what the flags dictate. (The demo runs in strict mode, so blocked operations report a real TypeError instead of failing silently.)
Object.defineProperties
Defining properties one at a time gets tedious. Object.defineProperties(obj, descriptors) sets several in a single call.
Object.defineProperties(obj, {
prop1: descriptor1,
prop2: descriptor2
// ...
});
Each key maps to its own descriptor:
Object.defineProperties(user, {
name: { value: "Maya", writable: false },
surname: { value: "Vance", writable: false },
// ...
});
One call, many properties, each with the exact flags you specify.
Object.getOwnPropertyDescriptors
The counterpart to reading a single descriptor is Object.getOwnPropertyDescriptors(obj), which returns descriptors for every property at once. Pair it with Object.defineProperties and you get a clone that preserves flags:
let clone = Object.defineProperties({}, Object.getOwnPropertyDescriptors(obj));
Why bother? The clone you usually write copies values through assignment:
for (let key in user) {
clone[key] = user[key]
}
That loop throws the flags away. Every copied property lands with the default true flags regardless of the original — a non-writable field becomes writable in the copy. The descriptor-based approach carries the flags across untouched.
✗ flags reset to defaults
✗ skips non-enumerable keys
✗ skips symbolic keys
✓ flags copied exactly
✓ includes non-enumerable keys
✓ includes symbolic keys
That second advantage matters as much as the first: for..in ignores symbolic properties and non-enumerable ones, so an assignment-based clone silently loses them. getOwnPropertyDescriptors reports all of a property’s descriptors, symbols and hidden keys included, so nothing slips through.
Sealing an object globally
Everything so far works property by property. Three more methods clamp down on the whole object at once, controlling whether its shape can grow, shrink, or change:
Object.preventExtensions(obj)— blocks adding new properties. Existing ones stay editable.Object.seal(obj)— blocks adding and removing properties, and setsconfigurable: falseon everything already there. Values can still change if they’re writable.Object.freeze(obj)— the hardest lock: no adding, no removing, no changing. It sets bothconfigurable: falseandwritable: falseacross the board.
Each has a matching test that reports the object’s current state:
Object.isExtensible(obj)— returnsfalsewhen new properties are forbidden, otherwisetrue.Object.isSealed(obj)— returnstruewhen extensions are blocked and every property isconfigurable: false.Object.isFrozen(obj)— returnstruewhen extensions are blocked and every property is bothconfigurable: falseandwritable: false.
Lock a config object at each level and then poke at it. Apply preventExtensions, seal, or freeze, then try to add, change, or delete a property — the badge shows the object’s reported state and each attempt reports whether the engine let it through.
In practice you’ll reach for these rarely. Object.freeze shows up now and then to protect a config object or a shared constant from accidental mutation; the rest are mostly things you’ll recognize when you meet them rather than tools you keep on your belt.