skip to content

Why does a property created with `Object.defineProperty(obj, 'id', { value: 1 })` behave differently in JavaScript from the same property created with `obj.id = 1`?

level: middleimportance: should knowfreq 50%

answer

  1. two different creation paths
  2. omitted attributes are not neutral
  3. false is the default for a new property
  4. redefinition keeps what you leave out
  5. spell out all four for an ordinary property

basics

~10 s

Attributes omitted from a defineProperty descriptor default to false, so the property is read-only, non-enumerable and non-configurable. Plain assignment creates it with writable, enumerable and configurable all true.

solid answer

~50 s

When `Object.defineProperty` **creates** a property, every attribute you do not mention defaults to `false`. So `{ value: 1 }` really means `{ value: 1, writable: false, enumerable: false, configurable: false }`: later assignments to `obj.id` are discarded in sloppy mode and throw a `TypeError` in strict mode, the key is skipped by key-listing and copying operations, and `delete obj.id` fails. Plain assignment goes through a different internal path that creates the property with `writable`, `enumerable` and `configurable` all `true`. The rule flips once the property exists: on a **redefinition** of an already-present configurable property, attributes you omit keep their current values, so `Object.defineProperty(obj, 'id', { value: 2 })` changes only the value. In practice, spell out all four attributes whenever you mean an ordinary property — the false-by-default behaviour is a deliberate design for hidden, locked-down internals, not a convenience.

go deeper

for a junior

Remember that Object.defineProperty is not a fancier =. A property created with only { value } cannot be reassigned, listed or deleted afterwards.

for a middle

State all three defaults and the strict-versus-sloppy difference in how a failed write reports itself, and explain the creation-versus-redefinition asymmetry that makes the false defaults conditional.

for a senior

Diagnose from the symptom: a field that reads back fine but disappears when copied points at enumerable: false, and a silently ignored assignment points at writable: false. Reach for the descriptor rather than guessing.

for a principal

Treat these defaults as an API-design lever — non-enumerable metadata and non-configurable invariants are contracts you impose on every future consumer, including yourself, and configurable: false in particular cannot be undone.

## Two different creation paths The surprise here comes from the fact that assignment and `Object.defineProperty` are not two spellings of the same operation. They run different internal algorithms with different defaults. ```js const a = {}; a.id = 1; Object.getOwnPropertyDescriptor(a, 'id'); // { value: 1, writable: true, enumerable: true, configurable: true } const b = {}; Object.defineProperty(b, 'id', { value: 1 }); Object.getOwnPropertyDescriptor(b, 'id'); // { value: 1, writable: false, enumerable: false, configurable: false } ``` Assignment creates an ordinary, fully open property. `Object.defineProperty` fills every unmentioned attribute with `false`, producing a property that is simultaneously read-only, hidden from key listings, undeletable and unreconfigurable. ## What each false costs you **`writable: false`** — the failure is quiet where you least want it: ```js b.id = 99; // sloppy mode: discarded, no error console.log(b.id); // 1 ``` Inside a class body, an ES module, or any `'use strict'` scope, the same assignment throws `TypeError: Cannot assign to read only property 'id' of object`. The mode difference is why the bug often reproduces in one file and not another. **`enumerable: false`** — the key stops appearing in the operations that walk enumerable keys, so the property survives in the object but vanishes from ordinary copies and from anything that iterates keys. Code that looks correct because `b.id` still reads back as 1 can lose the field entirely the moment the object is copied. **`configurable: false`** — `delete b.id` returns `false` in sloppy mode and throws in strict mode, and any later `Object.defineProperty` that tries to change the attributes throws `TypeError: Cannot redefine property: id`. This is a one-way door: nothing can reopen it. ## The rule flips for redefinition The false defaults apply only when the property is being **created**. If it already exists and is configurable, attributes you omit are left exactly as they were: ```js const c = { id: 1 }; // all three flags true Object.defineProperty(c, 'id', { value: 2 }); Object.getOwnPropertyDescriptor(c, 'id'); // { value: 2, writable: true, enumerable: true, configurable: true } ``` Only `value` changed. This asymmetry catches people who learn "defineProperty defaults everything to false" as an unconditional rule and then cannot explain why the same call behaves differently on a second object. The mental model that works: a descriptor passed to `defineProperty` is a **patch**, and creation patches a blank record whose fields all start `false`. ## The safe idiom If you want an ordinary property, say so: ```js Object.defineProperty(obj, 'id', { value: 1, writable: true, enumerable: true, configurable: true, }); ``` That is verbose on purpose. `Object.defineProperty` exists for the cases assignment cannot express — installing an accessor, hiding an internal field, freezing one key while leaving the rest open — and the terse form is optimised for the locked-down case. `Object.defineProperties(obj, { a: {...}, b: {...} })` applies several descriptors in one call, and `Object.create(proto, descriptorMap)` uses the same descriptor shape at construction time, with the same false-by-default rule. ## Assignment can also fail where defineProperty succeeds The two paths differ in the other direction as well. Assignment consults the prototype chain: if an inherited property is a getter-only accessor, or a non-writable data property, `obj.x = 1` refuses to create an own property (silently in sloppy mode, with a `TypeError` in strict mode). `Object.defineProperty` ignores inherited properties entirely and defines the own property regardless. That is why library and polyfill code that must install a property reliably reaches for `defineProperty` rather than assignment. ## Where the strict defaults are the right choice The defaults are not a design mistake — they suit exactly the job the API was built for: - attaching metadata a consumer should not see or copy (`enumerable: false`); - exposing a computed identifier that must never be overwritten (`writable: false`); - pinning something a plugin must not redefine (`configurable: false`). Built-ins are defined the same way: most methods on `Object.prototype`, `Array.prototype` and similar are writable and configurable but non-enumerable, which is why they never show up when you list an object's keys. Knowing the default therefore explains both your own bug and a chunk of the standard library's shape.

  • Does `Object.defineProperty` apply the same false defaults when the property already exists?
    No. The false defaults apply only when creating a new property. On an existing configurable property the descriptor acts as a patch: attributes you omit keep their current values, so `Object.defineProperty(obj, 'id', { value: 2 })` on a normal property changes the value and leaves `writable`, `enumerable` and `configurable` as they were.
  • Why do library authors reach for `Object.defineProperty` instead of plain assignment?
    Three reasons: it can install accessors, which assignment cannot express; it defines the own property regardless of what the prototype chain holds, whereas assignment is blocked by an inherited getter-only accessor or a non-writable inherited property; and its defaults make hidden or locked-down properties the terse case, which is exactly what metadata and polyfill code needs.

saying these in an interview costs you the question

  • Assumes omitted descriptor attributes default to true
  • Says defineProperty and assignment are interchangeable
  • Thinks the false defaults also apply when redefining an existing property
  • Expects a non-writable assignment to throw in every mode
  • Believes delete always removes an own property

context