skip to content

What does the optional second argument to `Object.create(proto, props)` expect, and what surprises people about the properties it defines?

level: middleimportance: nice to knowfreq 28%

answer

  1. descriptors, not values
  2. same shape as defineProperties
  3. omitted flags mean false
  4. invisible to keys and stringify
  5. silent in sloppy, throws in strict

basics

~20 s

It expects a map of property names to property descriptors, not plain values, and every flag you omit defaults to false — so the properties come out non-writable, non-enumerable and non-configurable unless you say otherwise.

solid answer

~40 s

The second argument has the same shape as the one `Object.defineProperties` takes: an object whose keys are the new property names and whose values are descriptor objects like `{ value: 1 }` or `{ get() {...} }`. Passing a plain value — `Object.create(p, { x: 1 })` — throws a TypeError, because `1` is not a descriptor. The real trap is the defaults: unlike ordinary assignment, a descriptor's `writable`, `enumerable` and `configurable` all default to `false`. So `Object.create(p, { x: { value: 1 } })` gives you a read-only, hidden, locked property: assigning to it fails silently in sloppy mode and throws in strict mode, and it never appears in `Object.keys`, `JSON.stringify` or spread. If you want the behaviour of a normal property you have to spell out all three flags.

code

javascript · 20 lines
javascript
'use strict';

const proto = { kind: 'point' };

const p = Object.create(proto, {
  x: { value: 1 },
  y: { value: 2, writable: true, enumerable: true, configurable: true },
});

console.log(p.x, p.y);                                  // 1 2
console.log(Object.keys(p));                            // [ 'y' ]
console.log(JSON.stringify(p));                         // {"y":2}
console.log(Object.getOwnPropertyDescriptor(p, 'x'));
// { value: 1, writable: false, enumerable: false, configurable: false }

try {
  p.x = 99;
} catch (err) {
  console.log(err.name);                                // TypeError in strict mode
}

go deeper

for a junior

Remember that the second argument takes descriptor objects such as { value: 1 }, not bare values, and that passing a bare value throws.

for a middle

Explain each defaulted flag and its observable effect: assignment failing, the property vanishing from Object.keys and JSON.stringify, and redefinition throwing once configurable is false.

for a senior

Demonstrate the diagnosis — a field that reads fine but never serializes — and reach for Object.getOwnPropertyDescriptor rather than guessing, while judging when locked-down properties are actually what the design wants.

for a principal

Weigh explicitness against ceremony in shared code: descriptor-created properties are precise but easy to misread, so decide where a codebase spells flags out and where plain assignment is the clearer contract.

## The signature `Object.create(proto)` sets a prototype. The two-argument form, `Object.create(proto, propertiesObject)`, additionally defines own properties on the new object — it is exactly `Object.create(proto)` followed by `Object.defineProperties(obj, propertiesObject)`. The second argument is therefore not a bag of values. Its own enumerable keys are the property names, and each corresponding value must be a **property descriptor**: either a data descriptor with `value` and optionally `writable`, or an accessor descriptor with `get` and/or `set` — plus `enumerable` and `configurable` on either kind. ```js const proto = { kind: 'point' }; Object.create(proto, { x: 1 }); // TypeError: Property description must be an object: 1 Object.create(proto, { x: { value: 1 } }); // works ``` The first form is the mistake almost everyone makes once: it reads like an object literal of initial values, and the engine rejects it because `1` is not a descriptor. ## The defaults are all false Here is the part that actually shows up in bugs. When you create a property by ordinary assignment (`obj.x = 1`), the resulting property is writable, enumerable and configurable. When you create it through a descriptor and omit a flag, the flag is `false`. ```js const p = Object.create(null, { x: { value: 1 } }); Object.getOwnPropertyDescriptor(p, 'x'); // { value: 1, writable: false, enumerable: false, configurable: false } ``` Each `false` has a visible consequence: - **`writable: false`** — `p.x = 99` does nothing in sloppy mode and throws `TypeError: Cannot assign to read only property 'x'` in strict mode. Module code and class bodies are strict, so in modern codebases this usually throws. - **`enumerable: false`** — the property is invisible to `Object.keys`, `Object.entries`, `for...in`, object spread, `Object.assign` and `JSON.stringify`. An object can look empty in a log or serialize to `{}` while still holding the data. - **`configurable: false`** — the property cannot be deleted, and its flags cannot be changed later. Redefining it with `Object.defineProperty` throws. This one is irreversible: you cannot loosen it afterwards, so the mistake has to be fixed at the creation site. To get properties that behave like ordinary assigned ones you must be explicit: ```js const q = Object.create(proto, { x: { value: 1, writable: true, enumerable: true, configurable: true }, }); Object.keys(q); // [ 'x' ] ``` ## When the strictness is the point The defaults are not a design flaw; they exist because a descriptor is the low-level, deliberate way to create a property, and the safe default for something you spell out by hand is "locked down". That makes the two-argument form genuinely useful when you want a fixed, hidden slot — an internal marker, a cached identity, or a constant that should not appear in serialization — installed in the same expression that establishes the prototype. Accessors work here too, which is the other reason to reach for the form: it is the only way to create an object with a getter *and* a specific prototype in one expression. ```js const counter = Object.create(proto, { count: { value: 0, writable: true }, doubled: { get() { return this.count * 2; }, enumerable: true }, }); ``` Note that `doubled` has no `value`/`writable`, because a descriptor may be a data descriptor or an accessor descriptor, never both — supplying `value` alongside `get` throws a TypeError. ## In practice Most code that just wants "an object with this prototype and these fields" is clearer written as `Object.create(proto)` followed by ordinary assignments or `Object.assign`, precisely because assignment gives the ordinary flags without ceremony. Reach for the descriptor argument when you specifically want the non-default flags or an accessor, and when you do, write all the flags out — an explicit `enumerable: true` costs one line and saves the next reader from checking the defaults. A good diagnostic habit: when a property visibly exists (`obj.x` returns a value, `'x' in obj` is true) but `Object.keys` and `JSON.stringify` disagree, inspect it with `Object.getOwnPropertyDescriptor` — you are almost certainly looking at a non-enumerable property created through a descriptor.

  • Why does `Object.create(proto, { x: 1 })` throw?
    Because the second argument's values must be property descriptors, and `1` is a primitive rather than a descriptor object. The engine reports `TypeError: Property description must be an object: 1`. The equivalent intent is written `{ x: { value: 1, writable: true, enumerable: true, configurable: true } }`, or more simply by assigning `x` after creating the object.
  • You defined a property with `{ value: 1 }` and later need it enumerable — can you fix it in place?
    No. `configurable` also defaulted to `false`, so redefining the property with `Object.defineProperty` throws `TypeError: Cannot redefine property`, and it cannot be deleted either. The only fix is at the creation site, or building a fresh object. That irreversibility is why the omitted flags matter more than they look.

saying these in an interview costs you the question

  • Passes plain values instead of descriptors
  • Assumes the defined properties behave like assigned ones
  • Thinks omitted descriptor flags default to true
  • Expects a non-enumerable property in JSON.stringify output
  • Believes a non-configurable property can be relaxed later

context