Property Descriptors
Every property of every JavaScript object has a small set of hidden flags called its descriptor. The descriptor decides whether the property can be reassigned, whether it shows up in loops, whether you can delete it, and whether reading it runs a getter. Most days you never think about descriptors — but they explain why some properties resist changes, why frozen objects behave the way they do, and how getters and setters fit in.
Inspecting a descriptor
Use Object.getOwnPropertyDescriptor to look at a property's flags.
const user = { name: "Ada" };
console.log(Object.getOwnPropertyDescriptor(user, "name"));{ value: 'Ada', writable: true, enumerable: true, configurable: true }Properties created by normal assignment or literal syntax are data descriptors with all three flags set to true. Those defaults are why you can normally do whatever you like with them.
The three boolean flags
writable— can the value be changed with=? Iffalse, assignments are silently ignored (or throw in strict mode).enumerable— does the property show up infor...in,Object.keys, spread, andJSON.stringify?configurable— can the property be deleted, or its descriptor changed? Setting this tofalseis one-way.
Object.defineProperty
To create a property with non-default flags — or to change them on an existing property — use Object.defineProperty. The third argument is the descriptor.
"use strict";
const user = {};
Object.defineProperty(user, "id", {
value: 42,
writable: false, // read-only
enumerable: true, // shows up in loops
configurable: false // cannot be deleted or reconfigured
});
console.log(user.id); // 42
try {
user.id = 99;
} catch (e) {
console.log("blocked:", e.message);
}Hiding properties from loops
Setting enumerable: false keeps a property out of Object.keys, JSON.stringify, and for...in, while still letting you read and assign to it.
const user = { name: "Ada" };
Object.defineProperty(user, "_secret", {
value: "hunter2",
writable: true,
enumerable: false,
configurable: true
});
console.log(user._secret); // 'hunter2'
console.log(Object.keys(user)); // [ 'name' ]
console.log(JSON.stringify(user)); // '{"name":"Ada"}'Getters and setters — accessor descriptors
A descriptor is either a data descriptor (value + writable) or an accessor descriptor (get + set). Accessor properties run functions when read or written, even though syntactically they look like a normal field.
const user = { firstName: "Ada", lastName: "Lovelace" };
Object.defineProperty(user, "fullName", {
get() { return `${this.firstName} ${this.lastName}`; },
set(value) {
[this.firstName, this.lastName] = value.split(" ");
},
enumerable: true,
configurable: true,
});
console.log(user.fullName); // 'Ada Lovelace'
user.fullName = "Grace Hopper";
console.log(user.firstName, user.lastName); // 'Grace' 'Hopper'The same pair can be written more compactly inside an object literal with get and set keywords — they desugar to the same descriptors.
defineProperties — many at once
const obj = {};
Object.defineProperties(obj, {
id: { value: 1, writable: false, enumerable: true, configurable: false },
tags: { value: [], writable: true, enumerable: true, configurable: true },
_log: { value: console.log, enumerable: false, configurable: false, writable: false },
});What freeze, seal and preventExtensions really do
These three "lockdown" methods are described in terms of descriptors.
Object.preventExtensions(o)— disallows adding new properties.Object.seal(o)—preventExtensionsplus sets every property'sconfigurabletofalse.Object.freeze(o)—sealplus sets every data property'swritabletofalse.
const cfg = Object.freeze({ port: 3000 });
console.log(Object.getOwnPropertyDescriptor(cfg, "port"));{ value: 3000, writable: false, enumerable: true, configurable: false }Configurable is a one-way switch
Once a property is set to configurable: false, the only descriptor change still allowed is flipping writable from true to false. You cannot un-freeze it, redefine it, or delete it.
"use strict";
const o = {};
Object.defineProperty(o, "x", { value: 1, configurable: false });
try {
Object.defineProperty(o, "x", { value: 2 }); // ok — same data descriptor with new value? No.
} catch (e) {
console.log("nope:", e.message);
}