JAVASCRIPT / PROTOTYPES, CLASSES, AND INHERITANCE
Object.create and delegation without classes
Build objects that delegate to other objects with Object.create, set own-property descriptors at creation, and make prototype-free maps with null.
What you will learn
- Create an object that delegates to any other object with Object.create(proto)
- Separate allocation from initialisation using an init method that returns this
- Define read-only or hidden own properties with Object.create's second argument
- Reach for Object.create(null) when you need a map with no inherited keys
Understanding Object.create and delegation without classes
Object.create(proto) allocates a brand new object, sets its internal prototype link to proto, and does nothing else. No function is called, no .prototype property is involved, and no initialisation code runs, so you get back an object with zero own properties whose failed lookups continue in proto. The proto argument is just whatever object you hand it: an object literal, another created object, SomeClass.prototype, or null.
The relationship this builds is delegation, not copying. In the example below savings does not contain deposit; it forwards the lookup to account, and because this is bound by the call site rather than by where the method was written, account.deposit ends up mutating savings. The link is live, so a method added to account after savings exists is immediately callable on savings, and a chain can be extended at any time by calling Object.create on an object that is already delegating.
The two services a class performs for you are exactly the ones you now handle by hand: initialisation and property attributes. A conventional init method that assigns the per-object data and returns this lets you write Object.create(proto).init(...) as a single expression. If a property must be read-only or invisible to Object.keys from the moment the object exists, pass a descriptor map as the second argument, keeping in mind that every flag you omit defaults to false, which is the opposite of what plain assignment gives you.
const account = {
init(owner, balance) {
this.owner = owner;
this.balance = balance;
return this;
},
deposit(amount) {
this.balance += amount;
return this.balance;
},
describe() {
return this.owner + ': ' + this.balance;
}
};
const savings = Object.create(account).init('Ada', 100);
savings.deposit(50);
console.log(savings.describe());
console.log(Object.getPrototypeOf(savings) === account);
console.log(Object.keys(savings).join(','));
console.log(savings.hasOwnProperty('deposit'), 'deposit' in savings);
const bonus = Object.create(savings);
bonus.deposit(10);
console.log(bonus.describe(), savings.describe());Object.create wires the prototype link directly, so any object can delegate to any other object with no constructor or class in between.
Worked examples
A dictionary with no prototype
Shows why Object.create(null) is the safe container for keys that come from user input.
const bare = Object.create(null);
const normal = {};
console.log('toString' in bare, 'toString' in normal);
bare.__proto__ = { hacked: true };
normal.__proto__ = { hacked: true };
console.log(Object.keys(bare).join(','));
console.log(Object.getPrototypeOf(bare), Object.getPrototypeOf(normal).hacked);Example explained
Line 1Object.create(null) produces an object with no prototype at all, so 'toString' in bare is false while the {} literal inherits it.
Line 2bare.__proto__ = ... stores an ordinary own key, because the __proto__ accessor lives on Object.prototype and bare does not have it.
Line 3The identical assignment on normal invokes that inherited setter and genuinely replaces normal's prototype, which is the prototype-pollution bug in one line.
Line 4Object.getPrototypeOf(bare) is still null after the assignment, which is the point of using it for untrusted keys.
The descriptor argument
Demonstrates that the second argument takes property descriptors whose flags default to false.
'use strict';
const proto = { greet() { return 'hi ' + this.name; } };
const locked = Object.create(proto, { name: { value: 'Ada' } });
const open = Object.create(proto, {
name: { value: 'Grace', writable: true, enumerable: true }
});
console.log(locked.greet(), open.greet());
console.log(JSON.stringify(locked), JSON.stringify(open));
try {
locked.name = 'Ada L.';
} catch (err) {
console.log(err.constructor.name);
}
console.log(locked.name);Example explained
Line 1Each entry in the second argument must be a descriptor object, which is why the value 'Ada' is wrapped in { value: 'Ada' }.
Line 2locked's name omits enumerable, so it defaults to false and JSON.stringify(locked) produces {} even though locked.greet() can read it.
Line 3writable also defaulted to false, so the assignment throws TypeError under 'use strict'; a sloppy-mode script would swallow it and still print Ada.
Line 4open spells out writable and enumerable, reproducing what a plain locked.name = 'Grace' assignment would have created.
Shared mutable state on the prototype
Shows the asymmetry between mutating through the prototype chain and assigning through it.
const listProto = {
items: [],
add(item) {
this.items.push(item);
return this;
}
};
const a = Object.create(listProto);
const b = Object.create(listProto);
a.add('x');
console.log(b.items.join(','), a.items === b.items);
const c = Object.create(listProto);
c.items = [];
c.add('y');
console.log(c.items.join(','), listProto.items.join(','));Example explained
Line 1a.add('x') finds no own items, so this.items resolves to the single array on listProto and push mutates that array in place.
Line 2a.items === b.items is true because neither object ever created an array of its own.
Line 3c.items = [] is an assignment, so it creates an own property on c and later add calls touch only c's array.
Line 4Mutation travels up the chain and writes to the prototype; assignment never does, which is why per-object data belongs in init.
Delegating to a class prototype
Confirms that class instances and Object.create objects use the same mechanism.
class Timer {
tick() { return ++this.ticks; }
}
const fake = Object.create(Timer.prototype);
fake.ticks = 0;
console.log(fake.tick(), fake.tick());
console.log(fake instanceof Timer);
console.log(Object.getPrototypeOf(fake) === Timer.prototype);
console.log(fake.constructor.name);Example explained
Line 1Object.create(Timer.prototype) links to the same object that new Timer() would have linked to, so tick is found normally.
Line 2instanceof only walks the prototype chain looking for Timer.prototype, so fake passes even though the constructor never ran.
Line 3fake.constructor is inherited from Timer.prototype, which is why it reports Timer rather than Object.
Line 4Nothing initialised fake, so ticks had to be assigned by hand; forget it and ++this.ticks yields NaN.
Important notes
Objects made with Object.create(null) have no toString, so `${obj}` or 'id: ' + obj throws 'Cannot convert object to primitive value'; log the object directly or use JSON.stringify.
Use Object.create when you are making the object; Object.setPrototypeOf rewires an object that already exists and engines de-optimise objects whose prototype changes after creation.
Common mistakes
Passing the constructor rather than its prototype: Object.create(Person) links to the function object, so Person.prototype methods are never found and the call dies with 'obj.greet is not a function'.
Treating the second argument as initial values: Object.create(proto, { name: 'Ada' }) throws TypeError: Property description must be an object: Ada, because each entry must be a descriptor.
Leaving an array or object literal on the shared prototype and mutating it with push, so every delegating object accumulates into the same list without any error.
Assuming Object.create runs setup code: nothing is initialised, so methods that read this.total on a fresh object get undefined and arithmetic produces NaN.
Try it yourself
Change, predict, then run
In a browser console define a shape object with a scale(n) method that multiplies this.w and this.h, then make two objects from it with Object.create and give each its own w and h. Scale one of them and confirm that the other object's numbers and shape itself are unchanged.
Open the JavaScript workspaceCheck your understanding
Given const proto = { count: 0, bump() { this.count++; } }; const a = Object.create(proto); const b = Object.create(proto); and then a.bump(); a.bump(); b.bump(); what are a.count, b.count and proto.count?
- 2, 1 and 0 — the first ++ on each object copies the prototype's value into a new own property
- 3, 3 and 3 — all three names read the single count that lives on the prototype
- 2, 1 and 3 — the increments also write through to the prototype's count
- 2, 1 and undefined — creating an own count removes the inherited one
Show answer
this.count++ is a read followed by an assignment. The read walks the chain and finds proto's 0, but assignment always lands on the receiver, so a and b each gain their own count seeded from 0 and proto.count is never touched. Option 1 is tempting because the read genuinely does hit the shared property, but only on the very first increment; after that the object is shadowing it. Contrast this with this.items.push(x), which mutates the prototype's array precisely because no assignment happens.