Learn JS Series (#36) - Object.create and Pure Prototypal Inheritance
Learn JS Series (#36) - Object.create and Pure Prototypal Inheritance
What will I learn
- You will learn to build inheritance directly with
Object.create, no classes involved; - how to chain prototypes so behaviour is shared across many objects at once;
- how to add an initializer method that sets up an object's own data;
- how to override an inherited method and still reach back to the original;
- why understanding this "pure prototypal" style makes the
classkeyword feel obvious later.
Requirements
- A working modern computer running macOS, Windows or Ubuntu;
- An installed Node.js (20+) distribution, or just a modern browser console;
- Episodes 1-35 read, especially the prototype (ep34) and the prototype chain (ep35).
Difficulty
- Intermediate
Curriculum (of the Learn JS Series):
- Learn JS Series (#1) - What Is JavaScript, Why It Runs Everywhere, and How to Run It
- Learn JS Series (#2) - Variables and Bindings
- Learn JS Series (#3) - The Primitive Types: number, string, boolean, null, undefined, symbol, bigint
- Learn JS Series (#4) - Operators and Expressions: Arithmetic, Comparison, Logical, and Short-Circuiting
- Learn JS Series (#5) - Strings: Template Literals, Unicode, and the Methods You Actually Use
- Learn JS Series (#6) - Numbers: IEEE 754, Why 0.1 + 0.2 Is Not 0.3, and How to Cope
- Learn JS Series (#7) - Control Flow: if/else, switch, and the Ternary Expression
- Learn JS Series (#8) - Loops: for, while, for...of, for...in, and When to Use Which
- Learn JS Series (#9) - Functions: Declarations, Parameters, Return Values, and Hoisting
- Learn JS Series (#10) - Scope and the Temporal Dead Zone: How JavaScript Finds Your Variables
- Learn JS Series (#11) - Arrays: The Workhorse Data Structure and Its Core Methods
- Learn JS Series (#12) - Objects: Key-Value Data, Dot vs Bracket Access, and Nesting
- Learn JS Series (#13) - Truthiness, Equality, and Coercion: == vs === Done Properly
- Learn JS Series (#14) - Mini Project: A Command-Line Tip Calculator
- Learn JS Series (#15) - First-Class Functions: Passing, Returning, and Storing Functions
- Learn JS Series (#16) - Arrow Functions vs function: Syntax, this, and When Each Wins
- Learn JS Series (#17) - Closures: The Single Most Important Idea in JavaScript
- Learn JS Series (#18) - Higher-Order Functions: Functions That Take or Return Functions
- Learn JS Series (#19) - Callbacks and the Callback Pattern (Before We Reach Promises)
- Learn JS Series (#20) - Default, Rest, and Spread: Flexible Function Signatures
- Learn JS Series (#21) - Destructuring Parameters: Named Arguments the JS Way
- Learn JS Series (#22) - The this Keyword: Five Rules That Explain Every Case
- Learn JS Series (#23) - call, apply, and bind: Controlling this Explicitly
- Learn JS Series (#24) - Recursion: Base Cases, the Call Stack, and Stack Overflows
- Learn JS Series (#25) - IIFEs and the Module Pattern (the Pre-2015 Way to Get Privacy)
- Learn JS Series (#26) - Currying and Partial Application
- Learn JS Series (#27) - Function Composition: Building Pipelines from Small Functions
- Learn JS Series (#28) - Pure Functions and Side Effects: The Foundation of Predictable Code
- Learn JS Series (#29) - Memoization: Trading Memory for Speed with Closures
- Learn JS Series (#30) - Mini Project: A Small Functional Utility Library
- Learn JS Series (#31) - Object Literals, Shorthand, and Computed Property Names
- Learn JS Series (#32) - Property Descriptors: writable, enumerable, configurable
- Learn JS Series (#33) - Getters and Setters: Computed Properties That Look Like Data
- Learn JS Series (#34) - The Prototype: JavaScript's Actual Inheritance Mechanism
- Learn JS Series (#35) - The Prototype Chain: How Property Lookup Really Works
- Learn JS Series (#36) - Object.create and Pure Prototypal Inheritance (this post)
Learn JS Series (#36) - Object.create and Pure Prototypal Inheritance
For two episodes now we have been observing the prototype system: episode 34 revealed that hidden link from one object to another, and episode 35 followed the whole chain up to Object.prototype and finally null, watching a property lookup climb it link by link. Today we stop watching and start building. We are going to construct inheritance ourselves, by hand, wiring one object to another with nothing but Object.create -- no constructors, no new, and above all no class keyword in sight.
I insist on this order for a reason. In two episodes you will meet class, and it will be tempting to treat it as some brand new machinery bolted onto the language. It is not. It is a thin layer of sugar over exactly the prototype links you are about to tie by hand. So if we build the raw thing first, the sugar will hold no mystery at all -- you will look at a class and see straight through it to the objects underneath. Having said that, let's earn it properly. ;-)
Solutions to Episode 35 Exercises
Exercise 1 - a three-level chain:
const base = { fromBase: "base" };
const middle = Object.create(base); middle.fromMiddle = "middle";
const leaf = Object.create(middle); leaf.fromLeaf = "leaf";
console.log(leaf.fromLeaf, leaf.fromMiddle, leaf.fromBase, leaf.nope);
// "leaf" "middle" "base" undefined
The insight: each lookup climbs only as far as it must -- fromLeaf is found at level 0, fromBase travels two links, and nope climbs to the top and returns undefined rather than throwing.
Exercise 2 - a plain object's chain:
const obj = { x: 1 };
console.log(Object.getPrototypeOf(obj) === Object.prototype); // true
console.log(Object.getPrototypeOf(Object.prototype) === null); // true
console.log(obj.toString()); // "[object Object]" - inherited from Object.prototype
The insight: the chain is obj -> Object.prototype -> null, and toString lives up on Object.prototype, which is exactly why every plain object seems to "just have" it.
Exercise 3 - bare versus normal object:
const normal = { a: 1 };
const bare = Object.create(null);
console.log(normal.hasOwnProperty("a")); // true
// bare.hasOwnProperty("a"); // TypeError: bare.hasOwnProperty is not a function
The insight: bare has no chain at all, so it never inherited hasOwnProperty; that same emptiness is what makes it safe as a dictionary -- no inherited keys can collide with your data.
Right, solutions behind us. Now we use the prototype system deliberately, as a tool, to assemble inheritance the pure JavaScript way.
Object.create in full
We have leaned on Object.create a handful of times already, but always in passing. Let's finally understand it properly, because it is the single most honest window into how inheritance actually works in this language. Object.create(proto) makes a brand new object whose prototype is proto. That is its entire job: create an object that is already linked to a prototype you choose. No constructor runs, no new is involved -- just one fresh object, pre-wired to an ancestor:
const animal = {
eat() { return `${this.name} is eating`; },
sleep() { return `${this.name} is sleeping`; },
};
const rabbit = Object.create(animal); // rabbit's prototype IS animal
rabbit.name = "Bugs";
console.log(rabbit.eat()); // "Bugs is eating"
console.log(rabbit.sleep()); // "Bugs is sleeping"
console.log(Object.getPrototypeOf(rabbit) === animal); // true
console.log(Object.hasOwn(rabbit, "eat")); // false - inherited, not own
Look closely at what each object owns. rabbit has exactly one own property, name. It does not have its own copy of eat or sleep -- those are found on animal through the prototype link, and Object.hasOwn confirms eat is not rabbit's own. When you call rabbit.eat(), the engine fails to find eat on rabbit, hops one link up to animal, finds it there, and runs it with this bound to rabbit (the object before the dot, exactly as episode 22 taught us). That is why this.name reads "Bugs" and not some property of animal. The behaviour lives in one shared place; the data lives per object. This is inheritance stripped down to its barest, most inspectable form.
An init method for setup
Setting properties one at a time after creation (rabbit.name = ...) is tedious and easy to get wrong -- forget a line and your object is half-built. A common and very idiomatic pattern is to put an initializer method on the prototype, conventionally named init, whose job is to create the object's own data and then return the object so calls can be chained:
const animal = {
init(name, sound) {
this.name = name; // creates an OWN property on the new object
this.sound = sound; // ditto
return this; // return the object so we can chain
},
speak() {
return `${this.name} says ${this.sound}`;
},
};
const cat = Object.create(animal).init("Whiskers", "meow");
console.log(cat.speak()); // "Whiskers says meow"
console.log(Object.hasOwn(cat, "name")); // true - name is cat's OWN property
console.log(Object.hasOwn(cat, "speak")); // false - speak is inherited
Read that one line Object.create(animal).init("Whiskers", "meow") slowly, because it is the whole episode in miniature. Object.create(animal) makes a new object linked to animal. That new object immediately inherits init, so .init("Whiskers", "meow") runs -- with this bound to the new object -- and stamps name and sound onto it as own properties. Because init returns this, the result of the whole expression is that fully set-up object, which we store in cat.
Notice the clean split of duties: creation (linking to a prototype) and initialization (setting up own data) are two separate steps here, happening one after the other. Keep that split in mind, because it is precisely the two jobs that the new operator (next episode) and the class constructor (episode 38) will bundle together into one call for you. Seeing them apart, done by hand, is what will make those feel obvious rather than magical.
Multi-level inheritance
Here is where the pure-object approach shows its quiet power. Because a prototype is just an object, and objects can themselves be created from other objects, you can stack as many levels of inheritance as you like with no special syntax. A specific prototype inherits from a general one, and your instances inherit from the specific one:
const animal = {
init(name) { this.name = name; return this; },
describe() { return `${this.name} is a ${this.type}`; },
};
// dog is an animal, with extra behaviour of its own:
const dog = Object.create(animal);
dog.type = "dog";
dog.fetch = function () { return `${this.name} fetches the ball`; };
const rex = Object.create(dog).init("Rex");
console.log(rex.describe()); // "Rex is a dog" - describe from animal, type from dog
console.log(rex.fetch()); // "Rex fetches the ball" - fetch from dog
console.log(rex.name); // "Rex" - rex's own property
The chain here is rex -> dog -> animal -> Object.prototype -> null, and every level pulls its weight. A lookup for fetch finds it one hop up on dog; a lookup for describe has to climb two hops to animal; type sits on dog; and name is rex's very own, set by init. The engine assembles the finished behaviour of rex by walking that chain, exactly as we studied last episode -- except now we built the chain on purpose, layer by layer.
There is a subtle and important detail in describe. It reads both this.name and this.type, yet those two properties live on completely different objects -- name on rex, type on dog. It works because this is always the object the method was called on (rex), not the object the method lives on (animal). So every this.something inside an inherited method triggers a fresh lookup starting from rex and climbing. This is the same layered assembly that class inheritance (episode 39) performs; classes will just give it a friendlier face.
Overriding, and calling the original
A more specific object can override an inherited method simply by defining its own property with the same name. This is shadowing, which we met in episode 34 -- the own property is found first at level 0, so the inherited one is never reached. But quite often you do not want to fully replace the parent's method; you want to extend it -- do what the parent did, and then a bit more. For that you call the prototype's version explicitly, keeping this pointed at the current object:
const base = {
greet() { return "hello"; },
};
const formal = Object.create(base);
formal.greet = function () {
const original = base.greet.call(this); // run the inherited version, this = formal
return `${original}, pleased to meet you`;
};
console.log(base.greet()); // "hello"
console.log(formal.greet()); // "hello, pleased to meet you"
formal.greet shadows base.greet, but inside it we reach back to the original with base.greet.call(this). That .call(this) is the crucial bit (episode 23's explicit this-binding at work): it runs base's greet but with this still set to formal, so if greet had read this.name it would read formal's data, not base's. This "override, but call the parent first" pattern is exactly what the super keyword will do for you inside classes in episode 39. Here you get to see the honest mechanism it hides: manually find the prototype's method and invoke it with the correct this. Once you have written it by hand, super will never be a black box.
One word of caution, so you do not learn a bad habit. Writing base.greet.call(this) hard-codes the parent object by name, which is fine for a two-level example but brittle in a deep chain. The robust, general way to reach "the method one level up from where I am" is via the prototype, using Object.getPrototypeOf, which we will lean on when we generalise this. For now, naming the parent directly keeps the idea crisp.
The second argument: property descriptors
Object.create has a lesser-known second parameter: a map of property descriptors (straight from episode 32) that defines own properties at creation time, with full control over their flags. It is verbose, and you will not reach for it every day, but it beautifully demonstrates that Object.create can both link a prototype and define own properties in a single call:
const proto = { greet() { return `hi, ${this.name}`; } };
const user = Object.create(proto, {
name: { value: "scipio", enumerable: true, writable: true },
id: { value: 42, enumerable: false, writable: false }, // hidden, locked own prop
});
console.log(user.greet()); // "hi, scipio"
console.log(Object.keys(user)); // ["name"] - id is non-enumerable, so it hides
console.log(user.id); // 42 - still readable, just not enumerable
user.id = 99; // silently ignored (writable:false; throws in strict mode)
console.log(user.id); // 42 - unchanged
Remember from episode 32 that a descriptor left to its defaults has every flag false, so id here is non-enumerable and non-writable -- it does not show up in Object.keys, and assignments to it are quietly dropped (or throw, under "use strict"). In everyday code you will almost always set own data with an init method or, later, a constructor, in stead of this descriptor form. But it is worth seeing once, because it makes concrete that "linking a prototype" and "defining own properties" are two independent jobs that Object.create happens to be able to do together.
A quick look sideways: how other languages spell "share behaviour"
A good few of you came into this series from the Learn Python Series (and some of you know Go or Rust), so the contrast is genuinely clarifying -- it shows that JavaScript's approach is one answer among several to a very universal need: let many values share one implementation without copying it.
In Python, you reach for a class, and the shared behaviour lives on that class. Structurally an instance-then-class lookup is the same "check here, not found, climb up" dance JavaScript does, but Python bakes the hierarchy into a formal class system rather than a chain of plain objects:
class Animal:
def __init__(self, name):
self.name = name # per-instance data
def describe(self):
return f"{self.name} is a {self.type}"
class Dog(Animal): # Dog inherits from Animal
type = "dog"
rex = Dog("Rex")
print(rex.describe()) # "Rex is a dog"
Notice how Python's __init__ is doing precisely the job of our init, and class Dog(Animal) is doing the job of our Object.create(animal) -- setting up the inheritance link. Same two ideas, different clothing. The philosophical difference is that Python gives you a privileged class/instance distinction computed up front (its MRO, method resolution order), whereas JavaScript hands you the raw, live chain of plain objects, which you can even re-wire at runtime.
Go and Rust take an entirely different road: neither has an inheritance chain at all. Go composes behaviour with struct embedding and interfaces; Rust resolves methods through traits and their implementations at compile time. So the same need gets three or four different answers, and JavaScript's answer -- the one you are wielding today -- is the inspectable, hand-buildable chain of objects. Understanding that it is a choice, not a quirk, makes the whole object model feel coherent rather than weird.
Why learn this before classes
You may be itching to reach the class keyword, and it is genuinely close now. But building inheritance by hand with Object.create first is a deliberate choice, and a valuable one. When you write your first class in episode 38, you will know, not merely be told, that it is not introducing some separate class-based type system underneath. It is setting up exactly the prototype links you just tied by hand.
Every part of class will map onto something you now understand from first principles: the methods you write go on a prototype (like animal's eat and speak); the constructor is an init that runs to set up own data and is bundled with creation; extends chains one prototype to another (like dog -> animal); and super calls the parent's method with the right this (like our base.greet.call(this)). That is the whole of class, and you have already built each piece with your own hands. That gap -- between using classes and understanding them -- is exactly why we took the long way round. ;-)
Try it yourself
- Create a prototype
shapewith aninit(name)method (setsthis.name, returnsthis) and anarea()method that returns0. Create acirclefrom it withObject.create, givecircleits ownarea()that computes from aradius, then make an instance and set its radius. Confirm the circle's ownarea()runs whilenamestill comes fromshape'sinit. - Build a two-level chain: a
personwith agreet()returning a plain greeting, and anemployeecreated frompersonthat overridesgreet()but callsperson.greet.call(this)inside and appends a job title. Make an employee instance and show that both parts of the greeting appear. - Use
Object.createwith its second (descriptors) argument to make an object linked to a prototype, giving it one enumerable own property and one non-enumerable own property. PrintObject.keys, print both property values directly, and explain in one sentence how this single call did two separate jobs.
So what did we actually cover?
Object.create(proto)makes a new object whose prototype isproto-- the most direct, honest way to set up prototypal inheritance, with nonewand noclass.- An
initmethod on the prototype sets up an object's own data and returnsthis, letting you create-then-init in one chained line -- the very two jobs thatnewandclasswill later bundle for you. - Multi-level inheritance is natural: a specific prototype inherits from a general one, instances inherit from the specific one, and the chain assembles the finished behaviour top to bottom.
- Override a method by shadowing it with an own property; extend the original by calling
Parent.method.call(this), which is exactly whatsuperwill do. Object.create's optional second argument defines own properties with full descriptor control at creation time, proving prototype-linking and property-defining are separate jobs.- Doing all of this by hand first means
classwill hold no mystery -- it is sugar over precisely these prototype links.
Next episode we take the older, function-based path to the very same result: constructor functions and the new operator, taking new apart step by step to see each thing it quietly does for you.