If you've learned object-oriented programming in languages like Java or C++, JavaScript's inheritance model will feel alien at first. There are no "real" classes in JavaScript (even with the class syntax introduced in ES6). Instead, JavaScript uses prototypes — a powerful and memory-efficient mechanism that's often misunderstood.
Here's the truth: Understanding prototypes is the key to understanding how JavaScript objects really work.
The Golden Rule
In JavaScript, objects inherit from other objects through a prototype chain. Every object has an internal [[Prototype]] reference that points to another object, and property lookups traverse this chain until the property is found or the chain ends.
In simpler terms: Objects link to other objects.
Let's demystify this from the ground up.
Part 1: What is a Prototype?
Every JavaScript object has a hidden internal property called [[Prototype]] (you can access it via __proto__ or Object.getPrototypeOf()).
When you try to access a property on an object, JavaScript:
- Checks if the object has that property directly
- If not, checks the object's prototype
- If not, checks the prototype's prototype
- Continues until it finds the property or reaches
null
This chain is called the prototype chain.
Basic Example
const animal = {
eats: true,
walk() {
console.log('Animal walks');
}
};
const rabbit = {
jumps: true
};
// Set rabbit's prototype to animal
Object.setPrototypeOf(rabbit, animal);
console.log(rabbit.jumps); // true (own property)
console.log(rabbit.eats); // true (inherited from animal)
rabbit.walk(); // "Animal walks" (inherited method)
What happened?
-
rabbitdoesn't have aneatsproperty, so JavaScript looks atrabbit.__proto__(which isanimal) - It finds
eats: truethere - Same for the
walk()method
Visualizing the Prototype Chain
rabbit = {
jumps: true,
__proto__: animal
}
animal = {
eats: true,
walk: function,
__proto__: Object.prototype
}
Object.prototype = {
toString: function,
hasOwnProperty: function,
...
__proto__: null
}
When you call rabbit.toString(), JavaScript:
- Checks
rabbit→ not found - Checks
animal→ not found - Checks
Object.prototype→ found!
Part 2: Constructor Functions and prototype
Before ES6 classes, JavaScript used constructor functions to create objects with shared methods.
The Old Way: Constructor Functions
function Animal(name) {
this.name = name; // Instance property
}
// Methods go on the prototype (shared by all instances)
Animal.prototype.walk = function() {
console.log(`${this.name} walks`);
};
const dog = new Animal('Dog');
const cat = new Animal('Cat');
dog.walk(); // "Dog walks"
cat.walk(); // "Cat walks"
console.log(dog.walk === cat.walk); // true (same function!)
Key Points:
-
Animal.prototypeis an object that becomes the[[Prototype]]of all instances created withnew Animal() - Methods on
prototypeare shared across all instances (memory efficient!) - Properties set in the constructor (
this.name) are unique to each instance
Why Put Methods on the Prototype?
Wrong way (memory inefficient):
function Animal(name) {
this.name = name;
this.walk = function() { // New function created for EVERY instance
console.log(`${this.name} walks`);
};
}
const dog = new Animal('Dog');
const cat = new Animal('Cat');
console.log(dog.walk === cat.walk); // false (different functions!)
Right way (memory efficient):
function Animal(name) {
this.name = name;
}
Animal.prototype.walk = function() { // One function shared by all
console.log(`${this.name} walks`);
};
const dog = new Animal('Dog');
const cat = new Animal('Cat');
console.log(dog.walk === cat.walk); // true (same function!)
Result: With 10,000 Animal instances, the prototype approach uses one walk function in memory, while the constructor approach creates 10,000 separate functions.
Part 3: __proto__ vs prototype
This is where confusion arises:
-
prototypeis a property on constructor functions (e.g.,Animal.prototype) -
__proto__(or[[Prototype]]) is an internal property on objects that points to their prototype
function Animal(name) {
this.name = name;
}
const dog = new Animal('Dog');
console.log(Animal.prototype); // Object with constructor and methods
console.log(dog.__proto__); // Same as Animal.prototype
console.log(dog.__proto__ === Animal.prototype); // true
Mental Model:
Animal (function)
├─ prototype (object)
├─ constructor: Animal
└─ walk: function
dog (instance)
├─ name: 'Dog'
└─ __proto__ → Animal.prototype
Part 4: ES6 Classes (Syntactic Sugar)
ES6 introduced the class syntax, but under the hood, it's still prototypes:
class Animal {
constructor(name) {
this.name = name; // Instance property
}
walk() { // Method on prototype
console.log(`${this.name} walks`);
}
}
const dog = new Animal('Dog');
dog.walk(); // "Dog walks"
console.log(typeof Animal); // "function" (it's still a constructor!)
console.log(dog.__proto__ === Animal.prototype); // true
What class does:
-
constructor()→ same as the old constructor function - Methods inside the class → added to
Animal.prototype -
new Animal()→ creates an instance with__proto__pointing toAnimal.prototype
Inheritance with Classes
class Animal {
constructor(name) {
this.name = name;
}
walk() {
console.log(`${this.name} walks`);
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name); // Call parent constructor
this.breed = breed;
}
bark() {
console.log(`${this.name} barks`);
}
}
const dog = new Dog('Buddy', 'Golden Retriever');
dog.walk(); // "Buddy walks" (inherited)
dog.bark(); // "Buddy barks" (own method)
console.log(dog.__proto__); // Dog.prototype
console.log(dog.__proto__.__proto__); // Animal.prototype
console.log(dog.__proto__.__proto__.__proto__); // Object.prototype
Prototype chain:
dog
└─ __proto__: Dog.prototype
└─ __proto__: Animal.prototype
└─ __proto__: Object.prototype
└─ __proto__: null
Part 5: Practical Prototype Patterns
Pattern 1: Checking Property Origin
const animal = { eats: true };
const rabbit = { jumps: true, __proto__: animal };
console.log('jumps' in rabbit); // true (own property)
console.log('eats' in rabbit); // true (inherited)
console.log(rabbit.hasOwnProperty('jumps')); // true
console.log(rabbit.hasOwnProperty('eats')); // false (not own)
Pattern 2: Iterating Over Own Properties
const animal = { eats: true };
const rabbit = { jumps: true, __proto__: animal };
for (let key in rabbit) {
console.log(key); // "jumps", "eats" (includes inherited!)
}
// Only own properties:
for (let key in rabbit) {
if (rabbit.hasOwnProperty(key)) {
console.log(key); // "jumps"
}
}
// Modern way:
console.log(Object.keys(rabbit)); // ["jumps"] (only own properties)
Pattern 3: Creating Objects Without Prototype
// Normal object has Object.prototype as prototype
const obj1 = {};
console.log(obj1.toString); // function (inherited from Object.prototype)
// Object without prototype (useful for dictionaries)
const obj2 = Object.create(null);
console.log(obj2.toString); // undefined (no prototype!)
Use case: Pure dictionaries where you don't want inherited properties:
const settings = Object.create(null);
settings.toString = 'Custom setting'; // No conflict with Object.prototype.toString
Part 6: Prototypes in React
While modern React doesn't heavily rely on prototypes (thanks to functional components and hooks), understanding prototypes helps when:
1. Working with Class Components (Legacy Code)
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
increment = () => { // Arrow function (own property, not on prototype)
this.setState({ count: this.state.count + 1 });
}
render() { // Method (on prototype)
return (
<div>
<p>Count: {this.state.count}</p>
<button onClick={this.increment}>Increment</button>
</div>
);
}
}
What's happening:
-
Counter.prototypehasrender(shared across all instances) -
incrementis an instance property (arrow function, not inherited) -
React.Component.prototypehassetState,forceUpdate, etc.
Prototype chain:
counterInstance
└─ __proto__: Counter.prototype
└─ __proto__: React.Component.prototype
└─ __proto__: Object.prototype
2. Understanding Method Binding in Class Components
The problem:
class Button extends React.Component {
handleClick() { // Regular method (on prototype)
console.log(this); // undefined in strict mode!
}
render() {
return <button onClick={this.handleClick}>Click</button>;
}
}
Why this is undefined:
-
this.handleClickpasses the function without binding - When React calls it,
thisis lost
Solutions:
// Solution 1: Arrow function (creates new instance property)
class Button extends React.Component {
handleClick = () => {
console.log(this); // Always correct
}
render() {
return <button onClick={this.handleClick}>Click</button>;
}
}
// Solution 2: Bind in constructor
class Button extends React.Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this); // Create bound version
}
handleClick() {
console.log(this);
}
render() {
return <button onClick={this.handleClick}>Click</button>;
}
}
// Solution 3: Arrow function in JSX (creates new function every render!)
class Button extends React.Component {
handleClick() {
console.log(this);
}
render() {
return <button onClick={() => this.handleClick()}>Click</button>;
}
}
3. Extending Component Libraries
Some libraries use prototypes for extensibility:
import { Component } from 'react';
// Adding a method to all React components (not recommended!)
Component.prototype.log = function(message) {
console.log(`[${this.constructor.name}] ${message}`);
};
class MyComponent extends Component {
componentDidMount() {
this.log('Mounted'); // Uses inherited method
}
render() {
return <div>Hello</div>;
}
}
Warning: Modifying built-in prototypes is generally not recommended in production code.
4. Functional Components Don't Use Prototypes
Modern React with hooks avoids prototype chains entirely:
function Counter() {
const [count, setCount] = useState(0);
const increment = () => { // Regular function, no prototype involved
setCount(count + 1);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
</div>
);
}
Why this works without prototypes:
- Functions are first-class citizens in JavaScript
- No need for
thisbinding - Closures handle state access (see Closures article!)
Part 7: Common Prototype Gotchas
Gotcha 1: Mutating Prototype Properties
function Animal() {}
Animal.prototype.friends = []; // Shared array!
const dog = new Animal();
const cat = new Animal();
dog.friends.push('Buddy');
console.log(cat.friends); // ["Buddy"] - Oops!
Why? The friends array is shared on the prototype. Mutating it affects all instances.
Fix: Initialize arrays/objects in the constructor:
function Animal() {
this.friends = []; // Each instance gets its own array
}
Gotcha 2: Shadowing Prototype Properties
const animal = {
eats: true
};
const rabbit = {
__proto__: animal
};
console.log(rabbit.eats); // true (inherited)
rabbit.eats = false; // Creates OWN property, doesn't modify prototype
console.log(rabbit.eats); // false (own property)
console.log(animal.eats); // true (prototype unchanged)
console.log(rabbit.__proto__.eats); // true
Key Point: Assigning to a property creates an own property, it doesn't modify the prototype.
Gotcha 3: Losing this in Callbacks
class Logger {
constructor(prefix) {
this.prefix = prefix;
}
log(message) { // Method on prototype
console.log(`${this.prefix}: ${message}`);
}
}
const logger = new Logger('INFO');
logger.log('Test'); // "INFO: Test"
setTimeout(logger.log, 1000, 'Test'); // Error: Cannot read 'prefix' of undefined
Why? When you pass logger.log to setTimeout, it loses the this binding.
Fix:
setTimeout(() => logger.log('Test'), 1000); // Arrow function preserves context
// or
setTimeout(logger.log.bind(logger), 1000, 'Test'); // Bind explicitly
Quick Reference Cheat Sheet
| Concept | Explanation | Example |
|---|---|---|
[[Prototype]] |
Internal property linking objects | obj.__proto__ |
prototype |
Property on constructor functions | Animal.prototype |
| Prototype Chain | Series of __proto__ links |
obj → proto1 → proto2 → null |
hasOwnProperty() |
Check if property is own (not inherited) | obj.hasOwnProperty('key') |
Object.create() |
Create object with specific prototype | Object.create(proto) |
Object.getPrototypeOf() |
Get object's prototype (modern) | Object.getPrototypeOf(obj) |
instanceof |
Check if object is in constructor's chain | dog instanceof Animal |
Key Takeaways
JavaScript uses prototypal inheritance, not classical inheritance
Methods on the prototype are shared across all instances (memory efficient)
Properties on the instance are unique to each object
__proto__ points to the object's prototype; prototype is on constructor functions
ES6 classes are syntactic sugar over constructor functions and prototypes
React class components use prototypes, but functional components don't
Never mutate shared arrays/objects on prototypes — initialize them in constructors
Arrow functions in class components create instance properties, not prototype methods
Interview Tip
When asked about prototypes, explain it in layers:
- "Every object has an internal prototype link (
__proto__)" - "When you access a property, JavaScript searches the prototype chain"
- "Constructor functions have a
prototypeproperty that becomes the__proto__of instances" - Give an example: "Methods on the prototype are shared for memory efficiency"
-
React connection: "Class components inherit from
React.Component.prototype, which providessetStateand other methods"
Now go forth and never confuse __proto__ and prototype again!
Top comments (0)