Table of Contents
- What is a Prototype?
- The Prototype Chain Explained
- How Objects Inherit from Prototypes
- Constructor Functions and Prototypes
- ES6 Classes and the Prototype Chain
- Common Pitfalls and Best Practices
- Conclusion
- References
What is a Prototype?
In JavaScript, every object has a prototype—an associated object from which it inherits properties and methods. Think of a prototype as a “template” or “parent” object that the original object can lean on for functionality it doesn’t define itself.
For example, consider a simple object created with object literal syntax:
const myObj = { a: 1 };
Even though myObj only explicitly defines the property a, it can access methods like toString() or hasOwnProperty(). These methods aren’t part of myObj itself—they’re inherited from myObj’s prototype.
How to Access an Object’s Prototype
To inspect an object’s prototype, JavaScript provides two primary tools:
Object.getPrototypeOf(obj): A standard method to retrieve the prototype of an object.obj.__proto__: A deprecated (but still widely recognized) property that points to the object’s prototype. Avoid using this in production code;Object.getPrototypeOf()is preferred for clarity and compatibility.
Let’s test this with myObj:
const myObj = { a: 1 };
console.log(Object.getPrototypeOf(myObj) === Object.prototype); // true
Here, myObj’s prototype is Object.prototype—the root prototype for most JavaScript objects.
The Prototype Chain Explained
The prototype chain is the hierarchical sequence of prototypes that JavaScript traverses when resolving a property or method access on an object. When you try to access a property (e.g., obj.property), JavaScript follows this chain until it either:
- Finds the property on one of the prototypes, or
- Reaches the end of the chain (
null), in which case it returnsundefined.
How the Chain Works: A Step-by-Step Example
Let’s create a simple object and trace the prototype chain:
// Create an object with a single property
const animal = { legs: 4 };
// Access a property NOT defined on `animal`
console.log(animal.toString()); // "[object Object]"
Why does animal.toString() work even though toString isn’t defined on animal? Let’s break down the chain:
- Check
animalitself:animalhaslegs: 4, but notoStringproperty. - Check
animal’s prototype:Object.getPrototypeOf(animal)isObject.prototype(the default prototype for objects created with literals).Object.prototypehas atoString()method. - Return the found property:
Object.prototype.toString()is executed, returning"[object Object]".
The End of the Chain: null
What if we try to access a property that doesn’t exist anywhere in the chain?
console.log(animal.nonExistentProperty); // undefined
Here’s the traversal:
animalhas nononExistentProperty.animal’s prototype (Object.prototype) also has nononExistentProperty.Object.prototype’s prototype isnull(the end of the chain).- JavaScript returns
undefined.
How Objects Inherit from Prototypes
JavaScript objects inherit from prototypes in three common ways:
1. Object Literals and new Object()
Objects created with literals ({ ... }) or new Object() inherit from Object.prototype by default:
const obj1 = {};
const obj2 = new Object();
console.log(Object.getPrototypeOf(obj1) === Object.prototype); // true
console.log(Object.getPrototypeOf(obj2) === Object.prototype); // true
2. Object.create(proto)
The Object.create(proto) method explicitly creates a new object with a specified prototype. This is useful for custom inheritance:
// Create a prototype object
const dogPrototype = {
bark: function() { return "Woof!"; }
};
// Create an object that inherits from dogPrototype
const myDog = Object.create(dogPrototype);
console.log(myDog.bark()); // "Woof!" (inherited from dogPrototype)
console.log(Object.getPrototypeOf(myDog) === dogPrototype); // true
Here, myDog’s prototype is dogPrototype, and dogPrototype’s prototype is Object.prototype (forming a chain: myDog → dogPrototype → Object.prototype → null).
3. Constructor Functions
Constructor functions (used with the new keyword) are a traditional way to create objects with shared behavior. Each constructor has a prototype property, which becomes the prototype for all instances created with new.
Constructor Functions and Prototypes
Constructor functions are blueprints for creating objects. When you define a constructor, its prototype property acts as the prototype for instances.
Example: A Person Constructor
// Define a constructor function
function Person(name) {
this.name = name; // Instance property
}
// Add a method to the constructor's prototype (shared by all instances)
Person.prototype.greet = function() {
return `Hello, my name is ${this.name}`;
};
// Create an instance with `new`
const alice = new Person("Alice");
console.log(alice.name); // "Alice" (instance property)
console.log(alice.greet()); // "Hello, my name is Alice" (inherited from Person.prototype)
Key Relationships in Constructors
- The instance (
alice) has a prototype (__proto__) pointing toPerson.prototype. - The constructor function (
Person) has aprototypeproperty pointing to the same object (Person.prototype). Person.prototypehas aconstructorproperty pointing back toPerson(useful for type checking):
console.log(Object.getPrototypeOf(alice) === Person.prototype); // true
console.log(Person.prototype.constructor === Person); // true
console.log(alice.constructor === Person); // true (inherited from Person.prototype)
The Prototype Chain for Instances
For alice, the chain is:
alice → Person.prototype → Object.prototype → null
ES6 Classes and the Prototype Chain
ES6 introduced class syntax, which is syntactic sugar over constructor functions and prototypes. Under the hood, classes still use the prototype chain for inheritance.
Basic Class Example
class Person {
constructor(name) {
this.name = name; // Instance property
}
greet() { // Method added to Person.prototype
return `Hello, my name is ${this.name}`;
}
}
const bob = new Person("Bob");
console.log(bob.greet()); // "Hello, my name is Bob"
This is equivalent to the earlier Person constructor example. The greet method is added to Person.prototype, and bob inherits from Person.prototype.
Inheritance with extends
The extends keyword enables prototype-based inheritance between classes. It sets up the prototype chain so that the child class’s prototype inherits from the parent class’s prototype.
// Parent class
class Animal {
constructor(legs) {
this.legs = legs;
}
walk() {
return `Walking on ${this.legs} legs`;
}
}
// Child class extending Animal
class Dog extends Animal {
constructor() {
super(4); // Call parent constructor (required in child constructors)
}
bark() {
return "Woof!";
}
}
const myDog = new Dog();
console.log(myDog.walk()); // "Walking on 4 legs" (inherited from Animal)
console.log(myDog.bark()); // "Woof!" (defined in Dog)
Prototype Chain for myDog:
myDog → Dog.prototype → Animal.prototype → Object.prototype → null
Common Pitfalls and Best Practices
1. Overwriting the Prototype Object
Avoid replacing the entire prototype object (e.g., Person.prototype = { ... }), as this breaks the constructor reference and can orphan existing instances:
function Person() {}
// Bad: Replaces the prototype object
Person.prototype = {
greet: function() { return "Hi"; }
};
const alice = new Person();
console.log(alice.constructor === Person); // false (constructor now points to Object)
Fix: Add properties to the existing prototype instead:
// Good: Extend the existing prototype
Person.prototype.greet = function() { return "Hi"; };
2. Confusing prototype and __proto__
Constructor.prototype: The prototype object for instances created withnew Constructor().instance.__proto__: The prototype of the instance (same asObject.getPrototypeOf(instance)).
3. Forgetting super() in Child Classes
In ES6 classes, child constructors must call super() before using this, as it initializes the parent class’s context:
class Child extends Parent {
constructor() {
// Error: Must call super constructor in derived class before accessing 'this'
this.age = 5;
super();
}
}
4. Relying on Inherited Properties in Loops
Use Object.hasOwnProperty() to check if a property is defined on the object itself (not inherited):
const obj = { a: 1 };
// Loop over all enumerable properties (including inherited ones)
for (const key in obj) {
if (obj.hasOwnProperty(key)) { // Check if key is "own" property
console.log(key); // "a" (only own property)
}
}
Conclusion
The prototype chain is the backbone of inheritance in JavaScript. It allows objects to inherit properties and methods from parent prototypes, enabling code reuse and flexible object design. While ES6 classes simplify syntax, they rely on the same prototype-based mechanism under the hood.
Key takeaways:
- Every object has a prototype, forming a chain ending in
null. - Properties are resolved by traversing the prototype chain.
- Constructors and ES6 classes use
prototypeto share behavior across instances. Object.create(),new, and classextendsare tools to manipulate the prototype chain.
By mastering the prototype chain, you’ll gain deeper insight into JavaScript’s object model and write more efficient, maintainable code.
References
- MDN Web Docs: Object.prototype
- MDN Web Docs: Prototype chain
- MDN Web Docs: Classes
- Simpson, K. (2014). You Don’t Know JS: this & Object Prototypes. O’Reilly Media.