Free lesson · JavaScript Basic to Advance
Object.create and Prototypal Inheritance
Building Objects Linked to Prototypes
The previous Topic showed Object.create in action without explaining it carefully. This Topic is dedicated to it: what it does, when to use it, and the patterns it enables.
const animal = {
eats: true,
walk() {
return `${this.name} is walking`;
}
};
const dog = Object.create(animal);
dog.name = "Rex";
console.log(dog.name); // "Rex"
console.log(dog.eats); // true — inherited
console.log(dog.walk()); // "Rex is walking"
Object.create(animal) creates a new empty object whose prototype is animal. That's its job. The new object starts with no own properties, but it has access to everything on animal through the prototype chain.
This is the cleanest way to set up prototypal inheritance in JavaScript. Classes (Module 8) wrap this mechanism with friendlier syntax, but Object.create is the underlying primitive. Understanding it makes the rest of JavaScript's object model clear.
Interactive animation: Links become a family tree - a lookup that misses on one object falls back to the next.
How Object.create Works
Object.create(proto) takes a prototype object and returns a new object with that prototype:
const proto = { greeting: "Hello" };
const obj = Object.create(proto);
console.log(obj); // {} — no own properties
console.log(obj.greeting); // "Hello" — inherited
console.log(Object.getPrototypeOf(obj) === proto); // true
The new object is empty. It has no own properties of its own. But its prototype chain links to proto, so reading inherited properties works.
You can also pass null to create an object with no prototype at all:
const orphan = Object.create(null);
console.log(orphan); // [Object: null prototype] {}
console.log(orphan.toString); // undefined — no inheritance from Object.prototype
console.log(Object.getPrototypeOf(orphan)); // null
Object.create(null) creates a "dictionary" object — an object that doesn't inherit from Object.prototype and therefore doesn't have methods like toString, hasOwnProperty, etc. This is useful when you want a clean key-value store with no risk of name collision with inherited methods.
Interactive animation: Object.create(null) - an object with nothing behind it, so no inherited methods like toString.
Creating Multiple Instances with Shared Behavior
The most common use of Object.create is creating multiple objects that share methods through a single prototype.
const userProto = {
greet() {
return `Hi, I'm ${this.name}`;
},
birthday() {
this.age++;
return this.age;
}
};
function makeUser(name, age) {
const user = Object.create(userProto);
user.name = name;
user.age = age;
return user;
}
const olena = makeUser("Olena", 35);
const nikhil = makeUser("Nikhil", 32);
console.log(olena.greet()); // "Hi, I'm Olena"
console.log(nikhil.greet()); // "Hi, I'm Nikhil"
console.log(olena.birthday()); // 36
console.log(nikhil.age); // 32 — independent
// All users share the SAME greet function via prototype
console.log(olena.greet === nikhil.greet); // true
makeUser is a factory function that creates new users. Each user is its own object with its own name and age. They all share greet and birthday through userProto. If you have 10,000 users, you have one shared greet function — not 10,000 copies.
This memory efficiency is one of the main reasons for using prototypal inheritance. It also makes method updates easy: change a method on the prototype, and every object using that prototype sees the change.
graph TD
A[userProto<br/>greet, birthday] --> B[olena<br/>name, age]
A --> C[nikhil<br/>name, age]
A --> D[emeka<br/>name, age]
A --> E[arun<br/>name, age]
style A fill:#7c3aed,color:#fff
This diagram shows the prototypal inheritance pattern. A single userProto object holds the shared methods like greet and birthday. Each instance — olena, nikhil, emeka, arun — has its own own properties for name and age, but they all link to the same userProto through their prototype chain. This means there's only one copy of each method in memory, regardless of how many user objects exist.
The Old "Constructor Function" Pattern
Before classes were added to JavaScript, the dominant inheritance pattern used constructor functions — regular functions called with new. You'll see this in older code and need to be able to read it.
function User(name, age) {
this.name = name;
this.age = age;
}
User.prototype.greet = function() {
return `Hi, I'm ${this.name}`;
};
User.prototype.birthday = function() {
this.age++;
return this.age;
};
const olena = new User("Olena", 35);
const nikhil = new User("Nikhil", 32);
console.log(olena.greet()); // "Hi, I'm Olena"
console.log(nikhil.greet()); // "Hi, I'm Nikhil"
console.log(olena.greet === nikhil.greet); // true — shared via prototype
The mechanism is the same as Object.create, just with different syntax. When you call new User(...):
- A new empty object is created
- The new object's prototype is set to
User.prototype - The function runs with
thisbound to the new object - The new object is returned (unless the function explicitly returns a different object)
The User.prototype object is where shared methods live. Anything attached to User.prototype becomes available on every instance created with new User.
This pattern is what classes wrap. class User { constructor() {...} greet() {...} } does essentially the same thing — set up a function that initializes an object, with shared methods on a prototype. Module 8 covers the class syntax in detail.
Adding Methods After Creation
Because the prototype is a live object, you can modify it at any time and existing instances see the changes:
const userProto = {
greet() {
return `Hi, I'm ${this.name}`;
}
};
const olena = Object.create(userProto);
olena.name = "Olena";
console.log(olena.greet()); // "Hi, I'm Olena"
// olena.farewell(); // would throw — no farewell method
// Add a method to the prototype AFTER creating olena
userProto.farewell = function() {
return `Goodbye from ${this.name}`;
};
console.log(olena.farewell()); // "Goodbye from Olena" — now it works
This live linkage is sometimes useful and sometimes dangerous. Useful: you can add capabilities to existing objects by extending their prototype. Dangerous: modifying prototypes at runtime can introduce bugs that are hard to trace.
The most dangerous case is modifying built-in prototypes:
// NEVER DO THIS in production code
Array.prototype.last = function() {
return this[this.length - 1];
};
const arr = [1, 2, 3];
console.log(arr.last()); // 3 — works!
This works, but now every array in your program has a .last() method. If a library you're using also defines a .last() method on arrays, they'll conflict. If a future JavaScript spec adds a built-in .last() method, the behavior might differ. The risk-reward ratio is terrible.
The rule: don't modify built-in prototypes. Add helper functions instead. If you really need a custom array-like class, create one (Module 8 covers classes).
Interactive animation: Defaults under, overrides on top - an own override wins, and later changes to the prototype reach every object.
When to Use Object.create
In modern code, classes (Module 8) have replaced Object.create for most "object with inheritance" use cases. But Object.create is still the right tool in several situations:
1. Building objects with no prototype — Object.create(null) for clean dictionaries.
2. Direct inheritance from a specific object — when you have an existing object and want to create another linked to it. This is occasionally useful when patching or testing.
3. Reading other people's code — Object.create and constructor functions are common in pre-class JavaScript. Knowing the mechanism makes the code readable.
4. Understanding what classes do underneath — once you've worked with Object.create directly, classes feel like a clean wrapper rather than magic.
For new code, you'll mostly use classes. But the underlying mechanism is what we covered here, and it remains JavaScript's actual inheritance model.
Common Pitfalls
Warning: The Object.create traps.
Mutating shared state on the prototype by accident. If a prototype has an array or object as a property, all instances share that same array or object. Pushing to it from one instance affects all others. Always initialize mutable state as own properties, not on the prototype.
Confusing
Object.create(obj)withObject.assign({}, obj).Object.createlinks a new object toobjvia prototype.Object.assigncopiesobj's properties to a new object (no link). Different mechanisms entirely.Forgetting that the constructor function pattern still uses prototypes underneath. When you see
new ConstructorFn(...)in old code, the mechanism is the same asObject.create(ConstructorFn.prototype)followed by initialization. Just different syntax.
Why This Matters in Interviews
Interview: "How does prototypal inheritance work in JavaScript?" is a senior-level question. The expected answer covers
Object.create(or the equivalent constructor pattern), the chain lookup, and the difference from classical class inheritance."What does
newdo?" is a classic intermediate question. The four steps: create empty object, set its prototype to the function's.prototypeproperty, run the function withthisbound to the new object, return the new object.Topic 7.8 — Interview Lens: Objects and Prototypes — covers the deeper interview treatment, including the "implement your own
new" challenge.
Try It
Exercise: Implement an
Accountsystem usingObject.create(no classes yet — Module 8 will cover those).
- Create an
accountProtoobject withdeposit(amount),withdraw(amount), andgetBalance()methods. Methods should usethis.balance.- Create a factory function
makeAccount(initialBalance)that creates a new object viaObject.create(accountProto), setsbalanceto the initial value, and returns the object.- Create two accounts and verify they have independent balances but share the same method implementations.
- Confirm that
account.deposit === otherAccount.depositistrue(same shared method).const accountProto = { // your methods here }; function makeAccount(initialBalance) { // your code } const a = makeAccount(100); const b = makeAccount(500); a.deposit(50); b.withdraw(100); console.log(a.getBalance()); console.log(b.getBalance()); console.log(a.deposit === b.deposit);Once this works, you've built prototypal inheritance from scratch. The class syntax in Module 8 will be a clean wrapper over exactly this pattern.