JavaScript Prototypes
Understand how JavaScript inheritance really works — from object literals and prototype chains to constructor functions, Object.create(), and ES6 classes as syntactic sugar.

Introduction
JavaScript is often described as a prototype-based language. Unlike classical inheritance in languages like Java or C++, JavaScript objects inherit behavior by linking to other objects through an internal prototype reference.
If you have used ES6 class syntax and assumed that is how inheritance works under the hood, this guide will change how you read JavaScript code. Classes are useful — but prototypes are the foundation.
This article walks through the full mental model:
- Core fundamentals — objects, functions, and prototype-based design
- Object creation issues — why manual literals do not scale
- The prototype concept — sharing methods and
Object.create() - Constructor functions &
new— automated object creation - The prototype chain — how property lookup works
- Modern JavaScript (ES6) — classes as syntactic sugar
Each section includes practical examples you can run in the browser console or Node.js.
Core Fundamentals
Before prototypes click, you need three foundational ideas clear in your mind.
Prototype-based language
JavaScript does not copy class blueprints into instances. Instead, each object can delegate property access to another object called its prototype. When a property is missing on the object itself, the engine walks the chain until it finds a match or reaches the end.
Everything is an object (almost)
Primitives (string, number, boolean, null, undefined, symbol, bigint) are not objects — but JavaScript wraps them when you access methods:
const name = 'Kazi'
name.toUpperCase() // 'KAZI' — temporary String object under the hoodArrays, functions, dates, and plain objects are all objects with internal prototype links.
Functions are objects too
Functions can hold properties and methods just like any other object:
function greet(name) {
return `Hello, ${name}`
}
greet.label = 'Greeter'
greet.version = 1
console.log(greet.label) // 'Greeter'
console.log(greet('Kazi')) // 'Hello, Kazi'Because functions are objects, they can also participate in the prototype system — which becomes essential when you use constructor functions.
Object Creation Issues
The simplest way to create an object is an object literal:
const user1 = {
name: 'Kazi',
role: 'Developer',
greet() {
return `Hi, I'm ${this.name}`
},
}
const user2 = {
name: 'Alex',
role: 'Designer',
greet() {
return `Hi, I'm ${this.name}`
},
}This works for one or two objects. It breaks down quickly at scale.
Manual object literals
Every new user requires copy-pasting the same structure. There is no single source of truth for shared behavior.
Code duplication
If greet changes, you must update it in every literal. Miss one and you get inconsistent behavior across instances.
Memory inefficiency
Each object carries its own copy of greet. With thousands of instances, the same function body is stored repeatedly in memory.
Performance impact
More duplicated functions mean more memory pressure and slower garbage collection — especially in large client-side apps or long-running Node.js servers.
The Prototype Concept
A prototype is the object an instance delegates to when it does not own a property locally.
Sharing methods and properties
Instead of attaching greet to every user, define it once on a shared parent:
const userPrototype = {
greet() {
return `Hi, I'm ${this.name}`
},
}
const user1 = Object.create(userPrototype)
user1.name = 'Kazi'
user1.role = 'Developer'
const user2 = Object.create(userPrototype)
user2.name = 'Alex'
user2.role = 'Designer'
console.log(user1.greet()) // "Hi, I'm Kazi"
console.log(user2.greet()) // "Hi, I'm Alex"Both user1 and user2 share the same greet function in memory.
Object.create(parent)
Object.create(proto) creates a new object whose internal [[Prototype]] points to proto:
const animal = {
eats: true,
describe() {
return this.name
},
}
const dog = Object.create(animal)
dog.name = 'Rex'
dog.barks = true
console.log(dog.eats) // true — inherited from animal
console.log(dog.describe()) // 'Rex'Parent-child inheritance
The child object (dog) inherits from the parent (animal). The parent does not know about the child — inheritance is a one-way lookup relationship.
Common method storage
Shared logic lives on the prototype. Instance-specific data (name, role, barks) lives on the object itself:
user1 ──► userPrototype ──► Object.prototype ──► null
│ │
name greet()
roleConstructor Functions & new
Constructor functions automate object creation and wire up the prototype link for you.
Automated object creation
A constructor is a regular function intended to be called with new:
function User(name, role) {
this.name = name
this.role = role
}
User.prototype.greet = function () {
return `Hi, I'm ${this.name}`
}
const user1 = new User('Kazi', 'Developer')
const user2 = new User('Alex', 'Designer')
console.log(user1.greet()) // "Hi, I'm Kazi"
console.log(user2.greet()) // "Hi, I'm Alex"The prototype property
Every function has a prototype property. When you call new User(), JavaScript:
- Creates a new empty object
- Sets the object's
[[Prototype]]toUser.prototype - Binds
thisto the new object inside the function - Returns the new object (unless the function returns its own object)
console.log(user1.__proto__ === User.prototype) // true (legacy accessor)
console.log(Object.getPrototypeOf(user1) === User.prototype) // true (preferred)Internal linkage
The instance does not store greet directly. It delegates to User.prototype:
console.log(user1.hasOwnProperty('name')) // true
console.log(user1.hasOwnProperty('greet')) // false
console.log('greet' in user1) // true — found on prototypeReference pointing
All instances created from the same constructor share one prototype object:
User.prototype.team = 'Engineering'
console.log(user1.team) // 'Engineering'
console.log(user2.team) // 'Engineering' — same referencePrototype Chain
When you access user1.greet, JavaScript does not stop at user1. It walks the prototype chain until it finds the property or hits null.
Property lookup mechanism
function Animal(name) {
this.name = name
}
Animal.prototype.eat = function () {
return `${this.name} is eating`
}
function Dog(name, breed) {
Animal.call(this, name)
this.breed = breed
}
Dog.prototype = Object.create(Animal.prototype)
Dog.prototype.constructor = Dog
Dog.prototype.bark = function () {
return `${this.name} says woof`
}
const rex = new Dog('Rex', 'Labrador')
console.log(rex.bark()) // 'Rex says woof'
console.log(rex.eat()) // 'Rex is eating' — found on Animal.prototypeMember access hierarchy
Lookup order for rex.eat:
rex
└── Dog.prototype
└── Animal.prototype
└── Object.prototype
└── nullInheritance structure
Object.prototype is the root of almost all object chains. It provides built-in methods like toString, hasOwnProperty, and valueOf:
console.log(rex.toString()) // '[object Object]'If a property is not found anywhere in the chain, the result is undefined (or a ReferenceError for undeclared variables).
console.log(rex.fly) // undefined — no fly on rex or any prototypeModern JavaScript (ES6)
ES6 introduced the class keyword. It looks like classical inheritance — but it is syntactic sugar over the prototype system.
JavaScript classes
class User {
constructor(name, role) {
this.name = name
this.role = role
}
greet() {
return `Hi, I'm ${this.name}`
}
static createGuest() {
return new User('Guest', 'Visitor')
}
}
const user = new User('Kazi', 'Developer')
console.log(user.greet()) // "Hi, I'm Kazi"Syntactic sugar
This class is equivalent to the constructor + prototype pattern:
// Roughly what the engine does under the hood
function User(name, role) {
this.name = name
this.role = role
}
User.prototype.greet = function () {
return `Hi, I'm ${this.name}`
}You can verify the prototypal nature:
console.log(typeof User) // 'function'
console.log(User.prototype.greet === user.greet) // false (bound differently)
console.log(user instanceof User) // true
console.log(Object.getPrototypeOf(user) === User.prototype) // trueClass inheritance still uses prototypes
extends sets up the prototype chain between constructor prototypes:
class Animal {
constructor(name) {
this.name = name
}
eat() {
return `${this.name} is eating`
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name)
this.breed = breed
}
bark() {
return `${this.name} says woof`
}
}
const rex = new Dog('Rex', 'Labrador')
console.log(rex.bark()) // 'Rex says woof'
console.log(rex.eat()) // 'Rex is eating'super() calls the parent constructor. Method lookup still walks the same prototype chain — Dog.prototype links to Animal.prototype.
Underlying prototypal nature
Whether you write class, constructor functions, or Object.create(), the mental model is the same:
| Approach | Shared methods live on | Instance links via |
|---|---|---|
| Object literal | Nowhere (duplicated) | N/A |
Object.create() | Parent object | [[Prototype]] |
Constructor + new | Constructor.prototype | [[Prototype]] |
ES6 class | Class prototype methods | [[Prototype]] |
Conclusion
JavaScript prototypes are not a legacy footnote — they are the core inheritance mechanism of the language. ES6 classes make the syntax familiar, but debugging, reading framework source code, and understanding performance all benefit from knowing the prototype chain.
Key takeaways:
- Avoid duplicating methods across object literals — use a shared prototype
Object.create()gives you explicit control over the parent object- Constructor functions +
newautomate instance creation and prototype wiring - Property lookup walks the chain: instance → prototype → … →
Object.prototype→null classis syntactic sugar — prototypes still power inheritance under the hood
The next time you see instanceof, Object.getPrototypeOf(), or wonder why a method appears on an object it was never assigned to — trace the prototype chain. That is where the answer lives.
Have questions about prototypes or inheritance patterns? Reach out via the contact page.
Subscribe to my newsletter
Stay up to date and get notified when I share new contents.
No spam ever, unsubscribe anytime