JavaScriptProperty Descriptors

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.

JS
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 =? If false, assignments are silently ignored (or throw in strict mode).

  • enumerable — does the property show up in for...in, Object.keys, spread, and JSON.stringify?

  • configurable — can the property be deleted, or its descriptor changed? Setting this to false is 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.

JS
"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);
}
Omitted flags default to false
When you build a descriptor with `defineProperty`, missing flags are `false`, not the friendly `true` you get from literal syntax. Always spell out the flags you want.
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.

JS
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.

JS
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

JS
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)preventExtensions plus sets every property's configurable to false.

  • Object.freeze(o)seal plus sets every data property's writable to false.

JS
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.

JS
"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);
}
When do real apps reach for this?
Most application code uses literal syntax and classes — descriptors stay invisible. They show up in library code: defining read-only `length` on array-likes, wrapping legacy APIs, building observable objects, and implementing decorators.
One sentence
Every property has `writable`, `enumerable`, `configurable` flags — or a `get`/`set` pair. `Object.defineProperty` lets you set them precisely; `freeze`, `seal`, and `Object.keys` are just convenient shortcuts on top.