Course: JavaScript Advanced
Lesson 5: Prototypes
Level
Advanced
Estimated Reading Time
10–15 minutes
Goal
By the end of this lesson, the learner will be able to:
- Explain how the prototype chain works and how JavaScript resolves property lookups
- Use
Object.createto set up prototype-based inheritance manually - Distinguish between own properties and inherited properties and explain the performance implications
Frontend Development Context
JavaScript's object system is prototype-based at its core. Even though ES6 classes look like classical inheritance, they are syntax sugar over the same prototype chain. When you define a class method, it is placed on the class's prototype object, not on each instance. When you call Array.prototype.map or Object.prototype.toString, you are directly using the prototype chain. Understanding this mechanism makes you a much more effective debugger and architect.
At the advanced level, knowing how prototypes work explains why instanceof checks sometimes fail across iframes, why methods defined in a class constructor (as opposed to on the prototype) use more memory, and how libraries like Lodash extend built-in types safely. It also gives you the mental model needed to evaluate trade-offs between prototype-based patterns, class-based patterns, and composition.
Explanation
Every JavaScript object has an internal link to another object called its prototype. When you access a property on an object, JavaScript first looks for it as an own property of that object. If not found, it follows the prototype link to the next object and looks there. This continues up the chain until either the property is found or the chain ends at null (which is Object.prototype's prototype). This traversal is the prototype chain.
When you create an object with a constructor function or a class, the new instance's prototype is set to the constructor's prototype property. Methods defined in a class body are placed on this shared prototype object, not copied onto each instance. This is why 1,000 instances of a class all share the same method references — they each have a prototype link to the same prototype object.
Object.create(proto) creates a new object with its prototype explicitly set to proto. This is the most direct way to set up prototype inheritance without constructors or classes. You can create an object that inherits from another plain object, giving you prototype-based inheritance without any class syntax.
Own properties and inherited properties behave differently in several ways. Object.keys() returns only own enumerable properties. hasOwnProperty() (or the newer Object.hasOwn()) checks only own properties. for...in iterates both own and inherited enumerable properties. JSON.stringify only serializes own enumerable properties. Knowing these distinctions prevents silent data bugs when working with inherited objects.
The prototype chain is also the mechanism behind method overriding. If an object defines a method with the same name as one on its prototype, the own property is found first during lookup and the prototype's version is never reached. This is JavaScript's version of method overriding, and it works the same way whether you use classes or manual prototype chains.
Code Example
This example shows manual prototype chain construction with Object.create, demonstrates the property lookup chain, and compares own vs. inherited properties.
// Base prototype object
const animalProto = {
breathe() {
return `${this.name} breathes air`;
},
describe() {
return `${this.name} is a ${this.type}`;
},
};
// Dog prototype inherits from animalProto
const dogProto = Object.create(animalProto);
dogProto.fetch = function () {
return `${this.name} fetches the ball`;
};
dogProto.speak = function () {
return `${this.name} says: Woof!`;
};
// Factory that creates dog instances via Object.create
function createDog(name, breed) {
const dog = Object.create(dogProto);
dog.name = name; // own property
dog.breed = breed; // own property
dog.type = "dog"; // own property
return dog;
}
const rex = createDog("Rex", "Labrador");
const max = createDog("Max", "Beagle");
console.log(rex.speak()); // "Rex says: Woof!" — dogProto method
console.log(rex.breathe()); // "Rex breathes air" — animalProto method
console.log(rex.describe()); // "Rex is a dog" — animalProto method
// Prototype chain: rex -> dogProto -> animalProto -> Object.prototype -> null
console.log(Object.getPrototypeOf(rex) === dogProto); // true
console.log(Object.getPrototypeOf(dogProto) === animalProto); // true
// Own properties vs inherited
console.log(Object.keys(rex)); // ["name", "breed", "type"] — own only
console.log("speak" in rex); // true — includes inherited
console.log(Object.hasOwn(rex, "speak")); // false — speak is on dogProto
console.log(Object.hasOwn(rex, "name")); // true
// Override on instance
rex.speak = function () {
return `${this.name} says: WOOF WOOF!`; // shadows dogProto.speak
};
console.log(rex.speak()); // "Rex says: WOOF WOOF!"
console.log(max.speak()); // "Max says: Woof!" — unaffected
Code Explanation
animalProto is a plain object that serves as the top of the custom prototype chain. dogProto is created with Object.create(animalProto), which sets animalProto as dogProto's prototype. Methods added to dogProto are available to all dogs but not to generic animals.
createDog creates individual dog instances with Object.create(dogProto). Each dog's own properties (name, breed, type) are set directly on the instance. No own speak or breathe exists on rex — those are looked up through the chain at call time.
Object.keys(rex) returns only ["name", "breed", "type"] because those are the only own enumerable properties. The in operator finds speak because it searches the full chain. Object.hasOwn confirms that speak lives on the prototype, not on rex.
When rex.speak is assigned a new function, that creates an own property on rex that shadows dogProto.speak. Subsequent calls to rex.speak() find the own property first and never reach the prototype. max.speak() is completely unaffected because the shadow only exists on rex.
Pattern Highlights
✅ Positive Pattern: Define shared methods on the prototype (or class body), not inside the constructor — this way all instances share one function reference in memory rather than each holding their own copy.
⚠️ Neutral Note: The prototype chain lookup traverses every level until the property is found or
nullis reached. Very deep inheritance chains can have a minor performance cost for property lookups in hot code paths, though this is rarely a practical issue.
❌ Negative Pattern: Do not modify
Object.prototypeor built-in prototypes likeArray.prototypein application code — adding properties there affects every object or array in the entire program and every library loaded alongside yours.
Common Mistakes
Mistake 1: Defining methods inside the constructor
Placing methods in the constructor creates a new function object for every instance, defeating the purpose of the prototype.
function Button(label) {
this.label = label;
this.click = function () { // new function created for EVERY instance
console.log("Clicked:", this.label);
};
}
Define Button.prototype.click instead (or use a class method body). All instances then share the same function reference, which saves memory with many instances.
Mistake 2: Forgetting that for...in includes inherited properties
Iterating an object with for...in without checking hasOwnProperty can process prototype methods as if they were data.
const settings = Object.create({ defaultTheme: "light" });
settings.fontSize = 14;
settings.language = "en";
for (const key in settings) {
console.log(key, settings[key]);
// logs: fontSize, language, AND defaultTheme from the prototype
}
Use Object.keys() to iterate only own properties, or guard with Object.hasOwn(settings, key) inside the loop.
Mistake 3: Overwriting the prototype instead of extending it
Replacing Constructor.prototype with a new object destroys the constructor property and breaks instanceof.
function Widget() {}
Widget.prototype = { // REPLACES the prototype object entirely
render() { return "rendered"; },
};
const w = new Widget();
console.log(w instanceof Widget); // true (prototype chain still works)
console.log(w.constructor === Widget); // false — constructor is now Object
Add methods to the existing prototype (Widget.prototype.render = ...) instead of replacing it, or restore constructor manually after the replacement.
Quick Recap
- Every object in JavaScript has a prototype link; property lookups traverse this chain until found or until
nullis reached. Object.create(proto)creates a new object withprotoas its prototype — the most direct way to establish inheritance without classes.- Own properties (those directly on the object) shadow same-named prototype properties;
Object.keys,hasOwnProperty, andJSON.stringifyonly operate on own properties. - Methods belong on the prototype (or class body), not in the constructor — shared prototype methods mean one function reference for all instances, not one per instance.