Property flags & descriptors
Learn how JavaScript property descriptors control writable, enumerable, and configurable behavior, and how definition differs from assignment.
- 01Read a property's flagsUse getOwnPropertyDescriptor and getOwnPropertyDescriptors to see the value and the three flags of a property.
- 02Set the flags yourselfUse Object.defineProperty for one property, and Object.defineProperties or Object.create for many.
- 03Predict what the flags allowTell whether an assignment, a delete, a list like Object.keys, or a new definition works or fails.
Every property has three flags
You already use object properties all the time: user.name, settings.theme, cart.total. So far, a property has looked like a name and a value. JavaScript keeps a little more for each property. It also keeps three yes-or-no settings, called flags.
A property descriptor is a plain object that lists the value and the flags of one property. This lesson is about data descriptors. A data descriptor has four parts:
value: the value the property holds.writable: can assignment, likeobj.x = 2, change the value?enumerable: does the property show up when JavaScript lists properties?configurable: can you delete the property or change its flags?
There is a second kind, the accessor descriptor. It has no value. Instead it has a get function, which runs when you read the property, and a set function, which runs when you assign to it. The next lesson, Getters & setters, is about this kind. Here is a quick look.
let displayName = "Asha";
const user = {};
Object.defineProperty(user, "name", {
get() { return displayName; },
set(value) { displayName = value; },
enumerable: true,
});
console.log(user.name);
user.name = "Mina";
console.log(user.name);The real name lives in the separate displayName variable. Line 7 reads user.name, so get runs and prints Asha. Line 8 assigns, so set runs and stores Mina. Line 9 prints Mina. One descriptor uses either value and writable, or get and set. It can't mix the two.
Picture a small panel of switches behind each property. Turn off writable, and the value is read-only. Turn off enumerable, and the property is left out of lists. Turn off configurable, and the panel itself is locked.
- In real life: A switch marked writable
- In JavaScript: Can assignment change the value?
- In real life: A switch marked enumerable
- In JavaScript: Does the property show up in lists?
- In real life: A switch marked configurable
- In JavaScript: Can you delete the property or flip the other switches?
The table shows each flag. Read across a row to see what happens when that flag is true and when it is false.
| Flag | What happens when it is true | What happens when it is false |
|---|---|---|
writable | Assignment can change the value. | Assignment can't change the value. In strict mode it throws TypeError. In sloppy mode nothing happens. |
enumerable | The property shows up in Object.keys, for...in loops, spread copies, and JSON.stringify. | You can still read the property by name, but those lists skip it. |
configurable | You can delete the property and change its flags. | You can't delete it or change its flags. The one change allowed is writable: true to false. |
These flags explain things you may have seen. You can't overwrite Math.PI. Some properties never show up in Object.keys. Libraries can keep their own notes on an object without those notes appearing in loops or in JSON text.
Object.defineProperty sets the flags
INTERACTIVEPlain assignment, like obj.x = 1, sets a value. You don't choose any flags. Object.defineProperty lets you set the value and the flags yourself. We call this defining a property. It takes three things:
- the object, like
account - the property name, like
"id" - a descriptor object with the value and flags
"use strict";
const account = {};
Object.defineProperty(account, "id", {
value: 42,
writable: false,
enumerable: true,
configurable: false,
});
console.log(account.id); // 42Lines 4 to 9 give account an id property:
value: 42:idholds 42.writable: false: assignment can't change it.enumerable: true: it shows up in lists likeObject.keys.configurable: false: you can't delete it or change its flags later.
id property holds 42 and three flags. Each flag answers one yes-or-no question about id.Line 1 turns on strict mode, the stricter set of JavaScript rules. This site's code runs in strict mode. In strict mode, a write that isn't allowed throws a TypeError, an error you can catch. Without strict mode (people call it sloppy mode), the same write does nothing and gives no error. The old value stays.
"use strict";const panel = {};Object.defineProperty(panel, "choice", { value: "open", writable: true, enumerable: true, configurable: true,}); panel.choice = "changed";delete panel.choice;Object.keys(panel);JSON.stringify(panel);- Before
{"value":"open","writable":true,"enumerable":true,"configurable":true}assignchangeddeletetruekeys(none)json{}redefineokAfter{"writable":false,"enumerable":false,"configurable":false}
A new object each time. With writable=true, enumerable=true, configurable=true: assign: changed
Try all three flags. Notice one surprise. With enumerable off, you can still read panel.choice. The flag only decides whether lists like Object.keys include it. And configurable is the flag that decides whether delete and redefining work.
Read a property's flags
STEP THROUGHJavaScript gives you two ways to read descriptors:
Object.getOwnPropertyDescriptor(obj, name)returns the descriptor of one property.Object.getOwnPropertyDescriptors(obj)returns the descriptors of all the object's properties at once.
Both look only at the object's own properties: the ones stored on the object itself. They don't look at its prototype, the object it borrows properties from.
Make one property with a literal and one with defineProperty. Then compare their flags.
script
"use strict";Object.defineProperty(normal, "quiet", { value: 2 }); console.log(Object.getOwnPropertyDescriptor(normal, "visible"));console.log(Object.getOwnPropertyDescriptor(normal, "quiet"));You can read the flags of built-in properties too. Math.PI has writable: false, enumerable: false, and configurable: false. So:
- strict code can't change it,
Object.keys(Math)doesn't list it,- and you can't delete it.
const pi = Object.getOwnPropertyDescriptor(Math, "PI");
console.log(pi.value);
console.log(pi.writable);
console.log(pi.configurable);Line 1 saves the descriptor of Math.PI in pi. Line 2 prints the value, 3.141592653589793. Lines 3 and 4 print false and false.
getOwnPropertyDescriptor only finds properties stored right on the object you pass. If an object gets a property from its prototype, you get undefined. To read that property's flags, pass the prototype object instead.
Default flags: assignment turns them on, defineProperty turns them off
This is the rule that surprises people most. How you make a property decides its starting flags:
- An object literal, like
{ x: 1 }, or an assignment, likeobj.x = 1, makes all three flagstrue. Object.definePropertymakes every flag you leave outfalse.
The table lists each way. Read a row to see the flags you get.
| When you write | The flags you get |
|---|---|
Object literal: { x: 1 } | writable: true, enumerable: true, configurable: true |
Assignment: obj.x = 1 | If the assignment works, all three flags are true. |
Object.defineProperty(obj, "x", { value: 1 }) | Every flag you leave out is false. The property is read-only, left out of lists, and locked. |
const assigned = {};
assigned.price = 3;
const defined = {};
Object.defineProperty(defined, "price", { value: 3 });
console.log(Object.getOwnPropertyDescriptor(assigned, "price").writable);
console.log(Object.getOwnPropertyDescriptor(defined, "price").writable);Line 2 uses assignment, so line 5 prints true for writable. Line 4 gives only a value, so line 6 prints false. The other two flags are false too, unless you write them in the descriptor.
price: 3, but the flags differ. Assignment turns all three flags on. defineProperty turns off every flag you leave out.This is on purpose. defineProperty is for when you want exact control. If it turned flags on by itself, you could open up a property by mistake.
const sloppy = {};
Object.defineProperty(sloppy, "x", { value: 1, writable: false });
sloppy.x = 2;
console.log(sloppy.x);
(function strictMode() {
"use strict";
const strict = {};
Object.defineProperty(strict, "x", { value: 1, writable: false });
try { strict.x = 2; } catch (error) { console.log(error.name); }
})();Lines 1 to 4 run in sloppy mode. Line 3 tries to change a read-only property. Nothing happens and there's no error, so line 4 prints 1. Lines 6 to 11 run in strict mode. The same write on line 10 throws, and the catch prints TypeError. This course's code is strict, so expect the error.
Define many properties at once
STEP THROUGHA descriptor map is an object where each key is a property name and each value is that property's descriptor. Two tools take a descriptor map:
Object.defineProperties(obj, map)adds the properties to an object you already have.Object.create(proto, map)makes a new object with those properties.
Make an object with Object.create, add more with Object.defineProperties, then compare Object.keys with every descriptor.
script
"use strict"; id: { value: 42, enumerable: true }, role: { value: "admin", enumerable: true },}); Object.defineProperties(user, { token: { value: "abc", writable: false }, label: { value: "Ada", writable: true, enumerable: true },}); console.log(Object.keys(user));console.log(Object.getOwnPropertyDescriptors(user));This is handy when an object needs some properties that everyone sees and some hidden ones. In the walkthrough, the descriptor for token leaves out enumerable, so it is false. You can still read user.token, but Object.keys and JSON.stringify skip it.
token has enumerable: false, so Object.keys skips it. Reading user.token by name still works.const original = {};
Object.defineProperties(original, {
id: { value: 42, writable: false, enumerable: true },
token: { value: "abc", enumerable: false },
});
const clone = Object.defineProperties({}, Object.getOwnPropertyDescriptors(original));
const spread = { ...original };
console.log(Object.keys(clone));
console.log(Object.getOwnPropertyDescriptor(clone, "token").enumerable);
console.log(Object.getOwnPropertyDescriptor(spread, "id").writable);Line 6 makes clone. getOwnPropertyDescriptors gives a descriptor map, and defineProperties uses it. So clone keeps token hidden and id read-only. Line 7 makes spread with the spread syntax ..., which copies only listed values. Its id is a normal property, so line 10 prints true for writable. Use the descriptor way when the copy must follow the same rules as the original.
"use strict";
const user = Object.create(null, {
id: { value: 42, enumerable: true },
role: { value: "admin", enumerable: true },
});
Object.defineProperties(user, {
token: { value: "abc", writable: false },
label: { value: "Ada", writable: true, enumerable: true },
});
console.log(Object.keys(user));
console.log(Object.getOwnPropertyDescriptors(user));Assignment vs definition
STEP THROUGHAssignment and definition both put a property on an object. But they follow different rules:
- Assignment, like
child.name = "Ada", first looks fornameon the object and its prototype. If the prototype has a setter (asetfunction for that name), the setter runs. If the prototype has a read-only property with that name, the assignment fails. - Definition, with
Object.defineProperty, puts the property straight onto the object. It doesn't check the prototype.
You go to write a name in a notebook. On an earlier page, a note says "Write all names in the guest list." So you follow the note instead. Gluing in a new labeled page is different. You skip the notes and add the page right where you want it.
- In real life: Writing on the page in the usual way
- In JavaScript: Assignment:
child.name = "Ada" - In real life: Following a note on an earlier page
- In JavaScript: An inherited setter that runs during assignment
- In real life: Gluing in a new labeled page
- In JavaScript: Definition:
Object.defineProperty(child, "name", ...)
Compare assignment, which checks the prototype, with definition, which doesn't.
script
"use strict";const log = []; set name(value) { log.push("setter:" + value); }};Object.defineProperty(proto, "locked", { value: 1, writable: false }); const child = Object.create(proto);child.name = "Ada";Object.defineProperty(child, "name", { value: "Ada", enumerable: true }); try { child.locked = 2; } catch (error) { log.push(error.name); }Object.defineProperty(child, "locked", { value: 2, enumerable: true }); console.log(log);console.log(child.name, child.locked);"use strict";
const log = [];
const proto = {
set name(value) { log.push("setter:" + value); }
};
Object.defineProperty(proto, "locked", { value: 1, writable: false });
const child = Object.create(proto);
child.name = "Ada";
Object.defineProperty(child, "name", { value: "Ada", enumerable: true });
try { child.locked = 2; } catch (error) { log.push(error.name); }
Object.defineProperty(child, "locked", { value: 2, enumerable: true });
console.log(log);
console.log(child.name, child.locked);Line 9 assigns, so the setter on proto runs and adds "setter:Ada" to log. Line 10 defines, so child gets its own name, and the setter doesn't run.
proto and runs it. Definition skips the setter and puts name right on child.Line 12 assigns to locked. The prototype's locked is read-only, so the assignment throws TypeError. Line 13 defines locked on child and works.
This matters for objects made from prototypes, like class instances and objects from libraries. Assign when you want the object's setters to run. Define when you need exact flags on the object itself.
Where you'll use this
Most properties don't need special flags. Descriptors help in a few places where a normal property allows too much:
- Read-only constants. A library can share a version number that other code can't overwrite by mistake.
- Hidden notes. Frameworks often store their own notes on your objects. With
enumerable: false, those notes stay out ofObject.keys, spread copies, and JSON text. - A property that can change kind later. With
configurable: true, you can later replace a plain value withgetandsetfunctions, as in the next lesson. - Finding bugs. Reading a property's flags tells you why a write, a loop, or a
deleteacts differently than you expected.
- Assign to a property with
writable: true - In strict mode, assign to an own property with
writable: false - Read
obj.secretwhensecrethasenumerable: false - See a non-enumerable property in
Object.keys(obj) - Delete a property with
configurable: true - Change
configurable: falseback totrue - Change
writable: truetofalseon a property withconfigurable: false - Use
definePropertyto add an own property when the prototype has a setter for that name
Sort each action by whether the flags allow it. Assume strict mode, so a failed write throws.
Common mistakes and the one-way lock
"Non-enumerable means private."
No. Lists like Object.keys skip it. But anyone who knows the name can read it, and getOwnPropertyDescriptors shows it.
"writable: false freezes the whole object."
No. It makes one property's value read-only. You can still add or change other properties. Freezing a whole object comes later in this module.
"configurable: false means the value can never change."
No. If writable is still true, assignment can still change the value. configurable decides only whether you can delete the property or change its flags.
"You can unlock a property later."
No. configurable: false works one way only. You may still change writable from true to false, but you can't change it back.
"Assignment and defineProperty do the same thing."
No. Assignment can run a setter from the prototype, and a read-only property on the prototype can block it. Definition puts the property straight onto the object.
Once a property has configurable: false, other code can trust that it stays that way. You can make it stricter by changing writable: true to false. You can never make it looser again.
"use strict";
const account = {};
Object.defineProperty(account, "id", {
value: 1,
writable: true,
configurable: false,
});
Object.defineProperty(account, "id", { writable: false });
console.log(Object.getOwnPropertyDescriptor(account, "id").writable);
try { Object.defineProperty(account, "id", { writable: true }); } catch (error) {
console.log(error.name);
}Lines 3 to 7 define id with configurable: false. Line 8 changes writable from true to false. That is allowed, because it only makes the property stricter. Line 9 prints false. Line 10 tries to turn writable back on, so it throws, and line 11 prints TypeError. You can still read the value.
Practice: property descriptors
5 EXERCISESRead the code and predict the two lines it prints. This is the smallest useful descriptor: a value plus flags.
"use strict";
const profile = {};
Object.defineProperty(profile, "id", {
value: 101,
writable: false,
enumerable: true,
configurable: true,
});
try { profile.id = 202; } catch (error) { console.log(error.name); }
console.log(profile.id);The assignment to profile.id fails with TypeError, and the catch prints that name. Then the last line prints 101, because the value didn't change. The property is read-only, not gone.
Work out the three lines it prints without running the code.
"use strict";
const user = {};
Object.defineProperty(user, "token", { value: "abc" });
console.log(Object.keys(user).length);
console.log(user.token);
try { delete user.token; } catch (error) { console.log(error.name); }Object.keys(user).length is 0, because token is not enumerable. user.token still reads abc. In strict mode, deleting a property with configurable: false throws TypeError.
The code uses Object.getOwnPropertyDescriptor on a built-in property. Which two values does it print?
const descriptor = Object.getOwnPropertyDescriptor(Math, "PI");
console.log(descriptor.writable);
console.log(descriptor.configurable);The descriptor says false for writable and false for configurable. That is why strict code can't change or delete Math.PI.
Which property name does Object.keys(settings) show?
const settings = { theme: "dark" };
Object.defineProperty(settings, "token", {
value: "abc",
enumerable: false,
});
console.log(Object.keys(settings));
console.log(settings.token);const settings = { theme: "dark" };
Object.defineProperty(settings, "token", {
value: "abc",
enumerable: false,
});
console.log(Object.keys(settings));
console.log(settings.token);The object literal made theme, so it is enumerable. token was defined with enumerable: false. So Object.keys(settings) shows only theme, and settings.token still reads abc.
What does the setter add to log? Why doesn't the definition line add anything?
"use strict";
const log = [];
const proto = { set name(value) { log.push(value); } };
const child = Object.create(proto);
child.name = "Ada";
Object.defineProperty(child, "name", { value: "Own Ada", enumerable: true });
console.log(log.join(","));
console.log(child.name);The setter adds only Ada, from the assignment on line 5. The defineProperty call on line 6 doesn't run the setter. It puts an own name with the value Own Ada on child.
Quiz: property flags
7 QUESTIONSQuestion 1 of 7Which call returns the descriptor of one own property?
Choose an answer to see the explanation.
Question 2 of 7What does this print?
Read the code, then predictconst box = { x: 1 }; console.log(Object.getOwnPropertyDescriptor(box, "x").writable);Choose an answer to see the explanation.
Question 3 of 7What does Object.keys give for a property made with only a value?
Read the code, then predictconst box = {}; Object.defineProperty(box, "x", { value: 1 }); console.log(Object.keys(box).length);Choose an answer to see the explanation.
Question 4 of 7Which flag decides whether
delete obj.xcan remove a property?Choose an answer to see the explanation.
Question 5 of 7What prints when strict code assigns to a read-only property on the prototype?
Read the code, then predict"use strict"; const parent = {}; Object.defineProperty(parent, "x", { value: 1, writable: false }); const child = Object.create(parent); try { child.x = 2; } catch (error) { console.log(error.name); }Choose an answer to see the explanation.
Question 6 of 7Which sentence about
configurable: falseis true?Choose an answer to see the explanation.
Question 7 of 7What prints when definition is used and the prototype has a setter?
Read the code, then predictconst log = []; const proto = { set name(value) { log.push(value); } }; const child = Object.create(proto); Object.defineProperty(child, "name", { value: "Ada" }); console.log(log.length);Choose an answer to see the explanation.
Key takeaways
- A property descriptor lists a property's value and its three flags.
writabledecides whether assignment can change the value.enumerabledecides whether lists likeObject.keysinclude the property.configurabledecides whether you can delete the property or change its flags.- Literals and assignment turn every flag on.
definePropertyturns off every flag you leave out. - Assignment checks the prototype for setters and read-only properties. Definition puts the property straight onto the object.
One-liner.
A descriptor holds a property's value and the flags that say whether it can be changed, listed, deleted, or given new flags.
Up next: Getters & setters.