skip to content

Object.create and Delegation

Building objects that delegate straight to another object, with no constructor or class involved. It comes up as the OLOO-versus-classes discussion and as the standard way to make a truly bare dictionary with no inherited keys at all.

part ofJavaScriptoverview, primer and where to startread it →
on this pageshow

questions

4

In JavaScript, given a `base` object that holds shared methods, what is the difference between creating a new object with `Object.create(base)` and creating one with `{ ...base }`?

level: juniorimportance: should knowfreq 60%

answer

  1. link versus snapshot
  2. one object owns nothing
  3. own enumerable only
  4. getters run once when copied
  5. later edits to the source

basics

~20 s

Object.create(base) returns an empty object linked to base, so every read falls through to base and later edits to base are visible. { ...base } copies base's own enumerable properties once into an unlinked object.

solid answer

~40 s

`Object.create(base)` returns a brand-new object with zero own properties whose prototype link is `base`. Every property read that misses walks up to `base`, so the relationship is live: a method added to `base` afterwards is immediately reachable from the child, and methods run with `this` bound to the child. `{ ...base }` does the opposite — it snapshots `base`'s own enumerable string and symbol properties into a fresh object whose prototype is `Object.prototype`. The child gets its own copies, never sees later edits to `base`, and inherited or non-enumerable properties are not copied at all. Spread also flattens accessors: a getter on `base` is invoked once and the result is stored as a plain value. Use `Object.create` when several objects should share one behaviour source, and spread when you want an independent snapshot.

code

javascript · 22 lines
javascript
const base = {
  greet() {
    return `hello ${this.name}`;
  },
};

const delegated = Object.create(base);
delegated.name = 'ada';

const copied = { ...base };
copied.name = 'ada';

base.shout = function () {
  return this.greet().toUpperCase();
};

console.log(delegated.greet());       // hello ada
console.log(copied.greet());          // hello ada
console.log(typeof delegated.shout);  // function  - live link to base
console.log(typeof copied.shout);     // undefined - snapshot taken earlier
console.log(Object.keys(delegated));  // [ 'name' ]
console.log(Object.keys(copied));     // [ 'greet', 'name' ]

go deeper

for a junior

Be able to say plainly that one links the new object to the source while the other copies its properties, and that copies do not notice later changes to the source.

for a middle

Explain exactly which property set spread copies — own, enumerable, string and symbol keys — and what that excludes: inherited members, non-enumerables, and accessors, which are invoked and flattened into values.

for a senior

Show where the choice causes real bugs: delegating objects that serialize as empty, spread class instances that lose their methods, and shared behaviour edited in one place versus copied into hundreds.

for a principal

Frame it as a data-versus-behaviour boundary: objects that cross serialization, storage, or worker boundaries should carry data copies, while behaviour is better shared through a single delegation source you can version and replace.

## Two different relationships, not two spellings of the same thing Both expressions hand you a new object that appears to "have" `base`'s members, which is why they get confused. But they encode opposite relationships. `Object.create(base)` builds a *delegation* link: the new object owns nothing and asks `base` at lookup time. Spread builds a *copy*: the new object owns everything and never asks anybody. ## What Object.create(base) builds `Object.create(proto)` is an ES5 built-in that allocates a fresh ordinary object and sets its internal prototype slot to `proto`. That is the whole operation — no constructor is called, no properties are copied. ```js const base = { greet() { return `hi ${this.name}`; } }; const child = Object.create(base); Object.keys(child); // [] - owns nothing Object.getPrototypeOf(child) === base; // true child.name = 'ada'; child.greet(); // 'hi ada' ``` Three consequences follow. **Reads are resolved on every access.** `child.greet` is not stored on `child`; the engine looks at `child`, misses, then looks at `base`. That means the link is live — `base.shout = ...` after the fact is instantly visible through `child`. **`this` is the receiver, not the holder.** When `child.greet()` runs, `this` is `child`, so one shared function body serves many delegating objects with different data. This is what makes delegation useful for behaviour rather than for data. **Own-property views ignore the chain.** `Object.keys`, `Object.entries`, `JSON.stringify` and object spread all look only at own enumerable properties, so a delegating object serializes as if it were nearly empty: ```js JSON.stringify(child); // {"name":"ada"} - greet is not own ``` That is a common surprise when a delegating object crosses a serialization boundary or gets structurally compared in a test. ## What spread and Object.assign build `{ ...base }` creates a plain object literal whose prototype is `Object.prototype`, then copies `base`'s **own enumerable** string- and symbol-keyed properties into it. `Object.assign(target, base)` copies the same set, with the difference that it assigns onto an existing target (so setters on the target run). What that copy set excludes matters: - **Inherited properties are not copied.** If `base` itself delegates to something, spreading `base` loses that layer entirely. - **Non-enumerable properties are not copied.** Class methods and anything defined with `Object.defineProperty` defaults are non-enumerable, so `{ ...someClassInstance }` gives you the fields and none of the methods. - **Accessors are flattened.** A getter is *invoked* during the copy and its return value is stored as an ordinary data property, so `{ ...base }` freezes a computed value in place instead of preserving the computation. - **The prototype is not carried over.** The result's prototype is always `Object.prototype`, regardless of what `base`'s was. ```js const live = { get now() { return Date.now(); } }; const snap = { ...live }; snap.now; // the same number forever ``` ## Which one do you actually want Reach for `Object.create(base)` when many objects should share one behaviour source and you want a single place to change it — the classic factory that returns `Object.create(methods)` and then fills in per-instance data. Reach for spread when you want an independent value: merging options, producing an updated record without mutating the original, or handing a plain data bag to code that will enumerate or serialize it. A useful tie-breaker is the question "do I want later changes to the source to show up here?" If yes, that is delegation. If no — and especially if the object is about to be JSON-encoded, diffed, or stored — you want a copy, and the copy should generally contain data only. The two also compose badly by accident. `{ ...Object.create(base) }` is an empty object, because the delegating object owns nothing. Likewise, spreading an object built by a class gives you fields without methods, which then fails the first time something calls a method on the copy. Being explicit about which relationship you are creating avoids both bugs.

  • If you spread an instance produced by a class, why do its methods disappear from the copy?
    Methods declared in a `class` body are installed on the constructor's prototype object and are marked non-enumerable. Spread copies only own enumerable properties, so it picks up the instance fields and nothing else. The copy is a plain data bag whose prototype is `Object.prototype`, and the first method call on it throws a TypeError.
  • What does `JSON.stringify` produce for an object built with `Object.create(base)`?
    Only its own enumerable properties. `JSON.stringify` never walks the prototype chain, so everything provided by `base` is absent from the output — a delegating object with no own data serializes as `{}`. If the shared members must survive serialization, they have to be copied onto the object or reconstructed on the other side.
  • Both `{ ...base }` and `Object.assign({}, base)` copy own enumerable properties — when does the choice between them matter?
    It matters when the target already exists. Spread always defines fresh properties on a brand-new object, while `Object.assign` performs assignment on the target, so any setter already present on the target runs and a non-writable property there causes a silent failure or a TypeError in strict mode. Into a fresh `{}` the two behave the same.

Delegation is a phone number for the expert; spread is a photocopy of their notes. When the expert learns something new, only the phone number stays current.

saying these in an interview costs you the question

  • Says Object.create copies the properties into the new object
  • Claims spread gives a deep copy of nested objects
  • Expects methods to survive spreading a class instance
  • Thinks Object.keys or JSON.stringify includes inherited members
  • Believes spread preserves getters as getters

context

open as a page

In JavaScript, why would you build a string-keyed lookup table with `Object.create(null)` instead of `{}`, and what stops working on the resulting object?

level: middleimportance: should knowfreq 50%

basics

~20 s

Object.create(null) makes an object with no prototype, so no key can collide with an inherited member such as toString or constructor and every property present is genuinely one you put there. The cost is that the object has no inherited methods at all.

open as a page

In JavaScript, the key `__proto__` behaves differently in an object literal, in the result of `JSON.parse`, and in a bracket assignment. Explain the three cases and what they mean for merging untrusted data into an object.

level: seniorimportance: should knowfreq 40%

basics

~20 s

In an object literal __proto__: v sets the new object's prototype instead of creating a property. JSON.parse creates an ordinary own property with that name. A bracket assignment on a normal object runs the inherited proto setter and reassigns the prototype, which is how naive merges reach Object.prototype.

open as a page

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%

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.

open as a page