Prototypes & Classes
Reviewed & published by Brayan K
Prototypes are the objects JavaScript uses to share properties and methods between instances, and classes are cleaner class syntax built on top of that prototype system for creating and inheriting objects.
Part of the free JavaScript course at LearnCodingFast β hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
Master JavaScript's object model, prototypal inheritance, and modern ES6 classes.
What You'll Learn in This Lesson
- Understand prototype-based inheritance
- How the prototype chain works
- Create objects with Object.create()
- Use ES6 Classes (syntactic sugar)
- Static methods and private fields
- Inheritance with 'extends' and 'super'
π‘ Running Code Locally: While this online editor runs real JavaScript, some advanced examples may have limitations. For the best experience:
- Download Node.js to run JavaScript on your computer
- Use your browser's Developer Console (Press F12) to test code snippets
- Create a .html file with <script> tags and open it in your browser
𧬠Real-World Analogy: Family Inheritance
Imagine a family tree. When you don't know how to do something (like cook a recipe), you ask your parent. If they don't know, they ask their parent, and so on up the family tree. In JavaScript, objects work the same way: when an object doesn't have a property or method, it "asks" its prototype, which asks its prototype, until it finds the answer or reaches the end of the chain.
Class = Blueprint (like a recipe) | Prototype = Family elder (where you inherit skills from)
π Prototypes vs Classes Quick Reference:
| Concept | Prototype-based | Class-based (ES6) |
|---|---|---|
| Object Creation | Object.create(proto) | new ClassName() |
| Inheritance | Prototype chain lookup | extends keyword |
| Method Sharing | Func.prototype.method | Defined inside class body |
| Under the Hood | The actual mechanism | Syntactic sugar over prototypes |
Introduction
JavaScript is one of the most misunderstood languages in the world when it comes to object-oriented programming. Developers coming from Java, Python, C#, C++, or even PHP often assume JavaScript follows the same classical inheritance model.
JavaScript uses a prototype-based inheritance system, which is extremely flexible, dynamic, and powerful. ES6 Classes add a familiar syntax, but they are syntactic sugarβthe underlying system is still prototypes all the way down.
This expert lesson will give you a true, deep, structural understanding of:
- How prototypes work
- How objects inherit properties
- What the [[Prototype]] internal slot is
- How the prototype chain is built
- Why functions have .prototype
- How ES6 Classes map onto prototypes
- How memory is shared between objects
- Real-world design patterns and architectures
1. Understanding JavaScript's Object Model
Everything in JavaScript is built on objects.
Even primitive values like strings, numbers, and booleans are temporarily wrapped in objects when you use methods on them.
Objects do not inherit from classes β they inherit from OTHER OBJECTS.
This is called prototype-based inheritance.
Every object in JavaScript has an internal hidden pointer: [[Prototype]]
This pointer links the object to another object. That other object acts as a fallback when the original object does not have a property.
const user = { name: "Alice" };
// Visually:
user ---> [[Prototype]] ---> Object.prototype ---> nullThis chain is called the prototype chain.
2. The Prototype Chain β How JavaScript Looks Up Properties
const user = { name: "Alice" };
console.log(user.toString());Does user have its own toString property? No.
- Does user have toString? β No
- Does user.[[Prototype]] (Object.prototype) have toString? β YES
- Use that one.
This system is known as: Prototype Delegation
Objects "delegate" missing behavior to their prototype.
3. Creating Objects and Their Prototypes
There are multiple ways to create objects with prototypes:
Method 1 β Object Literals
const user = { name: "Alice" };
// Prototype: Object.prototypeMethod 2 β Object.create()
const admin = Object.create(user);
admin.role = "Admin";
// Now: admin ---> user ---> Object.prototype ---> nullMethod 3 β Constructor Functions
function User(name) {
this.name = name;
}
// instance ---> User.prototype ---> Object.prototypeMethod 4 β ES6 Classes (Syntactic Sugar)
class User {
constructor(name) {
this.name = name;
}
}
// Behind the scenes: EXACT SAME as constructor functions + prototypes4. Functions and .prototype β Why Functions Create Objects
Every function you can call with new automatically gets a .prototype property.
One exception worth knowing: arrow functions and shorthand object methods do not get a .prototype, because they were never meant to be used with new. Evaluating (() => {}).prototype gives undefined, and new (() => {})() throws TypeError: f is not a constructor. Use a normal function or a class when you need to construct things.
function User() {}
console.log(User.prototype); // { constructor: User }const u = new User();- Create an empty object
- Set its [[Prototype]] = User.prototype
- Bind this inside User() to the new object
- Return the object
5. Building Methods on the Prototype
function User(name) {
this.name = name;
this.sayHello = function() {
return "Hello " + this.name;
};
}
// Every instance gets its OWN copy of sayHelloUser.prototype.sayHello = function() {
return "Hello " + this.name;
};
const u1 = new User("Alice");
const u2 = new User("Bob");
u1.sayHello === u2.sayHello; // trueNow ALL instances share ONE method stored in one place.
5b. Worked Example β Watch the Prototype Chain Do Its Job
Everything above has been diagrams and rules. Here it is as one program you can run. Read it line by line β each comment tells you what that line prints and why β then press Run and check the output matches.
// -- WORKED EXAMPLE - the prototype chain, one link at a time --
function User(name) {
// 'new User(...)' does three things for you:
// 1. makes a fresh empty object
// 2. points that object's hidden [[Prototype]] at User.prototype
// 3. runs this body with 'this' set to the new object
this.name = name; // an OWN property - one copy per instance
}
// This method is stored ONCE, on User.prototype. Instances do not copy it.
User.prototype.sayHello = function () {
return "Hello " + this.name; // 'this' is whichever instance called it
};
const u1 = new User("Alice");
const u2 = new User("Bob");
console.log(u1.sayHello()); // Hello Alice
console.log(u2.sayHello()); // Hello Bob
// Both instances point at the SAME function in memory - that is the saving:
console.log(u1.sayHello === u2.sayHello); // true
// 'name' belongs to the instance; 'sayHello' is found further up the chain:
console.log(u1.hasOwnProperty("name")); // true
console.log(u1.hasOwnProperty("sayHello")); // false
// Follow the chain by hand, one link at a time:
console.log(Object.getPrototypeOf(u1) === User.prototype); // true
console.log(Object.getPrototypeOf(User.prototype) === Object.prototype); // true
console.log(Object.getPrototypeOf(Object.prototype)); // null = end of chain
// SHADOWING: an own property always wins over the prototype's version.
u1.sayHello = function () { return "Alice does her own thing"; };
console.log(u1.sayHello()); // Alice does her own thing
console.log(u2.sayHello()); // Bob is untouched - still using the prototype
// Delete the own property and the prototype's version is visible again:
delete u1.sayHello;
console.log(u1.sayHello()); // Hello Alice
// Expected output:
// Hello Alice
// Hello Bob
// true
// true
// false
// true
// true
// null
// Alice does her own thing
// Hello Bob
// Hello AliceThe null in the middle of that output is the important one. It is JavaScript telling you the chain has ended β which is exactly why looking up a property that does not exist anywhere gives you undefined instead of an error.
6. ES6 Classes β Friendlier Syntax Over Prototypes
ES6 classes are syntactic sugar over the prototype system.
- Person is still a function
- Methods like greet live on Person.prototype
- new Person() sets up the prototype chain as before
π― Your Turn β Finish the Class Hierarchy
The whole program is written for you except three keywords β and those three keywords are exactly what this lesson teaches. Fill in the blanks and press Run.
// π― YOUR TURN - fill in the blanks marked ___
class Animal {
constructor(name) {
this.name = name;
}
speak() {
return this.name + " makes a sound";
}
describe() {
// Note: describe() calls speak(), but it does not care WHICH speak() -
// the object decides that at call time. Dog will exploit this.
return this.name + ": " + this.speak();
}
}
// π 1) Make Dog inherit everything Animal has.
// Replace ___ with the keyword that links one class to a parent class.
class Dog ___ Animal {
constructor(name, breed) {
// π 2) Run the parent constructor first, so 'name' actually gets set.
// Replace ___ with the keyword that means "the parent constructor".
// (Leave it out and JavaScript throws a ReferenceError on 'this'.)
___(name);
this.breed = breed;
}
// Overriding: same method name, different body. Dog's version wins for dogs.
speak() {
return this.name + " barks";
}
// π 3) This belongs to the CLASS itself, not to any one dog.
// Replace ___ with the keyword that makes a class-level method.
___ isGoodBoy() {
return true;
}
}
const generic = new Animal("Creature");
const rex = new Dog("Rex", "Beagle");
console.log(generic.describe());
console.log(rex.describe()); // inherited describe(), overridden speak()
console.log(rex.breed);
console.log(Dog.isGoodBoy()); // called on the class, not on rex
console.log(rex instanceof Animal); // a Dog is still an Animal
// β
Expected output:
// Creature: Creature makes a sound
// Rex: Rex barks
// Beagle
// true
// trueOnce it runs, try deleting the line with blank 2 entirely. You get ReferenceError: Must call super constructor in derived class before accessing 'this' β that is JavaScript insisting the parent gets to set up the object first.
Class Inheritance
Extend classes with extends and super
Static Methods
class MathHelper {
static add(a, b) {
return a + b;
}
static multiply(a, b) {
return a * b;
}
}
MathHelper.add(5, 3); // 8
// Not: new MathHelper().add()Getters & Setters
class Circle {
constructor(radius) {
this._radius = radius;
}
get area() {
return Math.PI * this._radius ** 2;
}
set radius(value) {
if (value > 0) this._radius = value;
}
}Private Fields
class BankAccount {
#balance = 0; // Private
deposit(amount) {
this.#balance += amount;
}
getBalance() {
return this.#balance;
}
}Summary: What You Learned
π You are now operating at a senior-level JavaScript OOP understanding.
π Quick Reference β OOP in JS
| Feature | Syntax |
|---|---|
| Class | class User { ... } |
| Constructor | constructor(name) { this.name = name } |
| Inheritance | class Dog extends Animal |
| Static | static create() { ... } |
| Private | #secret = "hidden" |
π Mini-Challenge: The Playlist Class
No blanks this time β just a brief, an outline, and a test harness that will tell you when you are right. Build a Playlist class that keeps its track list genuinely private, exposes two computed values through getters, and offers one static factory method.
// π― MINI-CHALLENGE: build the Playlist class
//
// Outline - no logic filled in, that part is yours:
//
// class Playlist {
// 1. a PRIVATE field #tracks starting as an empty array
// 2. constructor(name) - store name as a normal, public property
// 3. add(title, seconds) - push { title, seconds } onto #tracks, then
// 'return this' so calls can be chained: mix.add(...).add(...)
// 4. a GETTER named 'count' - how many tracks are in the playlist
// 5. a GETTER named 'totalTime' - the total as "M:SS"
// hint: Math.floor(total / 60) + ":" + String(total % 60).padStart(2, "0")
// 6. a STATIC method fromTitles(name, titles) that returns a new Playlist
// where every title becomes a 200-second track
// }
//
// Until you have written the class, running this throws
// "ReferenceError: Playlist is not defined" - that is expected, not a bug.
// your code here
// --- test harness: do not change anything below this line ---
const mix = new Playlist("Focus");
mix.add("Nocturne", 245).add("Rain", 180).add("Static", 60);
console.log(mix.name);
console.log(mix.count);
console.log(mix.totalTime);
console.log(mix.tracks); // must stay hidden - there is no public 'tracks'
const quick = Playlist.fromTitles("Quick", ["A", "B"]);
console.log(quick.count, quick.totalTime);
// β
Expected output:
// Focus
// 3
// 8:05
// undefined
// 2 6:40The undefined on line four of the expected output is the test that matters. A # field is not just a naming convention like _tracks β it is genuinely unreachable from outside the class body, so mix.tracks finds nothing.
Lesson Complete β Prototypes & Classes!
You now understand the architecture of JavaScript objects. This is critical for understanding frameworks like React and Vue under the hood.
Practice quiz
In JavaScript, what do objects inherit from?
- Classes
- Interfaces
- Other objects (their prototype)
- The global scope
Answer: Other objects (their prototype). JavaScript uses prototype-based inheritance: objects inherit from other objects, not from classes.
What is the internal hidden pointer every object uses to link to its prototype?
- [[Prototype]]
- __class__
- [[Parent]]
- this
Answer: [[Prototype]]. Every object has an internal [[Prototype]] slot pointing to another object used as a fallback.
When you read user.toString() and user has no own toString, where is it found?
- It throws
- It returns undefined
- On the window object
- On Object.prototype via the prototype chain
Answer: On Object.prototype via the prototype chain. JavaScript delegates up the prototype chain and finds toString on Object.prototype.
What are ES6 classes, under the hood?
- A brand-new inheritance model
- Syntactic sugar over the prototype system
- Faster than prototypes
- Only usable in TypeScript
Answer: Syntactic sugar over the prototype system. ES6 classes are syntactic sugar; the underlying mechanism is still prototypes.
Why is putting a method on User.prototype better than defining it inside the constructor?
- All instances share one method instead of each getting its own copy
- It runs faster to define
- It makes the method private
- It avoids the prototype chain
Answer: All instances share one method instead of each getting its own copy. Methods on the prototype are shared across all instances, saving memory versus a per-instance copy.
What does Object.create(user) produce?
- A copy of user's properties
- A frozen version of user
- A new object whose prototype is user
- An array of user's keys
Answer: A new object whose prototype is user. Object.create(user) makes a new object linked so that user is its prototype.
How do you call a parent constructor from a subclass constructor?
- this.parent()
- super(...)
- extends(...)
- Object.parent()
Answer: super(...). Inside a subclass constructor, super(...) invokes the parent class constructor.
How is a static method like MathHelper.add(5, 3) called?
- On an instance: new MathHelper().add()
- Only inside the constructor
- Through the prototype chain
- On the class itself, not on an instance
Answer: On the class itself, not on an instance. Static methods belong to the class itself and are called as MathHelper.add(5, 3), not on instances.
What syntax declares a truly private class field?
- _name
- #name
- private name
- this.name
Answer: #name. Prefixing a field with # (e.g. #balance) makes it genuinely private to the class.
What does typeof Person evaluate to for a class Person { }?
- 'class'
- 'object'
- 'function'
- 'undefined'
Answer: 'function'. A class is still a function under the hood, so typeof returns 'function'.
Continue this course
- Previous: Closures and Scope
- Next: Design Patterns β Apply classic patterns like Module, Observer, and Factory in JavaScript
- Quick reference: JavaScript cheat sheet