AlgoInsight

Free lesson · JavaScript Basic to Advance

Object.create and Prototypal Inheritance

Easy5 min read

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(...):

  1. A new empty object is created
  2. The new object's prototype is set to User.prototype
  3. The function runs with this bound to the new object
  4. 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) with Object.assign({}, obj). Object.create links a new object to obj via prototype. Object.assign copies obj'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 as Object.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 new do?" is a classic intermediate question. The four steps: create empty object, set its prototype to the function's .prototype property, run the function with this bound 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 Account system using Object.create (no classes yet — Module 8 will cover those).

  1. Create an accountProto object with deposit(amount), withdraw(amount), and getBalance() methods. Methods should use this.balance.
  2. Create a factory function makeAccount(initialBalance) that creates a new object via Object.create(accountProto), sets balance to the initial value, and returns the object.
  3. Create two accounts and verify they have independent balances but share the same method implementations.
  4. Confirm that account.deposit === otherAccount.deposit is true (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.

Back to JavaScript Basic to Advance