JavaScript Prototypes, Inheritance, Classes, and Encapsulation
When I first started learning JavaScript objects, prototype, Object.create(), class, inheritance, getters, setters, and private fields felt like completely different topics.
After working through them, I started seeing that most of them are connected through one main idea:
JavaScript objects use prototypes underneath.
This is my beginner-friendly understanding of the topic, with the examples I used while learning.
What I am learning
In this note, I am connecting:
- Prototype and prototype chain
Object.create()- Constructor functions
- Constructor function + prototype
- Inheritance
-
classand why it works with prototypes -
extendsandsuper - Static members
- Getters and setters
- Encapsulation with closures
- Private fields using
# - How all these concepts fit together
1. First: What is a Prototype?
A prototype is another object that JavaScript can look at when it cannot find a property or method directly on the current object.
A simple example:
const animal = {
eat() {
console.log("Eating");
}
};
const dog = Object.create(animal);
dog.bark = function () {
console.log("Barking");
};
dog.bark();
dog.eat();
Output:
Barking
Eating
The important thing is that eat() is not actually inside dog.
JavaScript finds it through dog's prototype.
dog
↓
animal
↓
Object.prototype
↓
null
So I can think of a prototype as:
"If you don't have something, JavaScript can look here next."
2. Prototype Chain
The prototype chain is simply the path JavaScript follows when looking for a property or method.
For example:
const person = {
name: "Koushik"
};
console.log(person.toString());
I never created toString() inside person.
So how does it work?
JavaScript searches:
person
↓
Object.prototype
↓
null
toString() is found on Object.prototype.
Property lookup in simple steps
When I write:
object.someProperty
JavaScript roughly does this:
- Look inside
object. - If it finds the property, use it.
- If not, look at the object's prototype.
- Keep moving up the prototype chain.
- If it reaches
null, the property was not found.
For example:
const animal = {
eat() {
console.log("Eating");
}
};
const dog = Object.create(animal);
dog.eat();
The search is:
dog
↓
animal ← eat() found here
↓
Object.prototype
↓
null
3. prototype vs __proto__
These names confused me at first, so I keep this distinction clear.
prototype
prototype is commonly seen with constructor functions and classes.
Example:
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function () {
console.log(`Hello ${this.name}`);
};
const person = new Person("Koushik");
person.sayHello();
The object created by new Person() can find sayHello() through:
Person.prototype
The chain is:
person
↓
Person.prototype
↓
Object.prototype
↓
null
__proto__
__proto__ gives access to an object's actual prototype.
console.log(person.__proto__ === Person.prototype);
Output:
true
For modern code, I should prefer:
Object.getPrototypeOf(person);
instead of directly using __proto__.
4. Object.create()
Object.create() creates a new object and lets me choose its prototype.
Syntax:
Object.create(prototype);
Example:
const animal = {
eat() {
console.log("Eating");
}
};
const dog = Object.create(animal);
dog.bark = function () {
console.log("Barking");
};
dog.eat();
dog.bark();
The relationship is:
dog
↓
animal
This is the simplest form of object-to-object inheritance.
My simple way of remembering it
const child = Object.create(parent);
means:
"Create a new object whose prototype is
parent."
5. Object.create() Inheritance
This is the first inheritance pattern I learned.
const obj = {
name: "Koushik",
age: 25,
getinfo() {
console.log(`My name is ${this.name}`);
}
};
const obj2 = Object.create(obj);
obj2.getage = function () {
console.log(`My age is ${this.age}`);
};
obj2.getinfo();
obj2.getage();
Here:
obj2
↓
obj
obj2 can use getinfo() because JavaScript finds it in obj.
So this is:
Object-to-object inheritance.
This is simple and useful when I already have an object that I want another object to be based on.
6. Constructor Functions
Before understanding inheritance with constructor functions, I first need to understand what a constructor function does.
Example:
function Bike(company, colour, cc) {
this.company = company;
this.colour = colour;
this.cc = cc;
}
I can think of this as a blueprint.
Then:
const bike1 = new Bike("Yamaha", "Blue", 1000);
creates an object.
Conceptually:
bike1
├── company → "Yamaha"
├── colour → "Blue"
└── cc → 1000
The new keyword is important because it creates a new object and connects that object to Bike.prototype.
7. Constructor Function + Prototype
Now I can add methods to the prototype.
My example:
function Bike(company, colour, cc) {
this.company = company;
this.colour = colour;
this.cc = cc;
}
Bike.prototype.fun = function () {
console.log(
"Hi, is this " +
this.company + " " +
this.colour +
" colour bike. This looks good. Is this " +
this.cc + "?"
);
};
const bike1 = new Bike("Yamaha", "Blue", 1000);
bike1.fun();
The important thing is that fun() is on:
Bike.prototype
not copied directly into every object.
The chain is:
bike1
↓
Bike.prototype
↓
Object.prototype
↓
null
When I call:
bike1.fun();
JavaScript looks for fun().
It checks:
bike1 → not found
Bike.prototype → found
So the method runs.
8. Important: Constructor + Prototype Is Not Automatically Inheritance
This was an important distinction for me.
If I write:
function Bike(company, colour, cc) {
this.company = company;
this.colour = colour;
this.cc = cc;
}
Bike.prototype.fun = function () {
console.log("Bike method");
};
const bike1 = new Bike("Yamaha", "Blue", 1000);
I have:
- A constructor function
- A prototype
- An instance
But I do not yet have a parent-child inheritance relationship.
The chain is only:
bike1
↓
Bike.prototype
↓
Object.prototype
↓
null
Actual constructor-function inheritance needs a second constructor.
9. Constructor Function + Prototype Inheritance
Now I can make:
Bike
↓
SuperBike
Here is my example:
function Bike(company, colour, cc) {
this.company = company;
this.colour = colour;
this.cc = cc;
}
Bike.prototype.fun = function () {
console.log(
"Hi, is this " +
this.company + " " +
this.colour +
" colour bike. This looks good. Is this " +
this.cc + "?"
);
};
function SuperBike(company, colour, cc, topspeed) {
Bike.call(this, company, colour, cc);
this.topspeed = topspeed;
}
// Actual prototype inheritance
SuperBike.prototype = Object.create(Bike.prototype);
// SuperBike's own method
SuperBike.prototype.showSpeed = function () {
console.log(
this.company +
" has a top speed of " +
this.topspeed +
" km/h"
);
};
const bike1 = new SuperBike(
"Yamaha",
"Blue",
1000,
300
);
bike1.fun();
bike1.showSpeed();
Where does inheritance actually happen?
This line:
SuperBike.prototype = Object.create(Bike.prototype);
is the important inheritance line.
It creates:
bike1
↓
SuperBike.prototype
↓
Bike.prototype
↓
Object.prototype
↓
null
So:
bike1.fun();
works because JavaScript eventually finds fun() in Bike.prototype.
10. Why Bike.call()?
This line:
Bike.call(this, company, colour, cc);
handles the parent's properties.
The parent constructor has:
function Bike(company, colour, cc) {
this.company = company;
this.colour = colour;
this.cc = cc;
}
When Bike.call(this, ...) runs inside SuperBike, this refers to the new SuperBike object.
So it effectively sets:
this.company = company;
this.colour = colour;
this.cc = cc;
Then SuperBike adds:
this.topspeed = topspeed;
So I remember the pattern like this:
Bike.call(...)
↓
Get parent's properties
Object.create(Bike.prototype)
↓
Get parent's prototype methods
11. Object.create() vs Constructor Inheritance
This was one of the things I wanted to understand clearly.
Object.create()
const child = Object.create(parent);
This is:
"Make this object inherit from that object."
Simple object-to-object inheritance.
Constructor + prototype
function Parent() {}
Parent.prototype.method = function () {};
const child = new Parent();
This is mainly:
"Create many instances from a constructor and share methods through the prototype."
It is not automatically parent-child inheritance.
Actual constructor inheritance
Child.prototype = Object.create(Parent.prototype);
Now I have:
Child
↓
Parent
So I do not need to think of Object.create() and constructor inheritance as unrelated topics.
Object.create() is actually part of the old-style inheritance pattern.
12. Classes
Modern JavaScript gives us class.
Example:
class Bike {
constructor(company, colour, cc) {
this.company = company;
this.colour = colour;
this.cc = cc;
}
fun() {
console.log(
`This is a ${this.colour} ${this.company} bike`
);
}
}
const bike1 = new Bike("Yamaha", "Blue", 1000);
bike1.fun();
This looks very different from:
function Bike(company, colour, cc) {
this.company = company;
this.colour = colour;
this.cc = cc;
}
Bike.prototype.fun = function () {
// ...
};
But the important thing is:
Classes still use prototypes underneath.
The class method is placed on:
Bike.prototype
rather than copied separately into every instance.
So:
class syntax
↓
prototype system underneath
This is why classes are often described as syntactic sugar over the prototype-based system.
13. Class Inheritance with extends
With classes, inheritance becomes much cleaner.
class Bike {
constructor(company, colour) {
this.company = company;
this.colour = colour;
}
ride() {
console.log(`${this.company} is riding`);
}
}
class SuperBike extends Bike {
showSpeed() {
console.log("Top speed is 300 km/h");
}
}
const bike = new SuperBike("Yamaha", "Blue");
bike.ride();
bike.showSpeed();
The inheritance chain is approximately:
bike
↓
SuperBike.prototype
↓
Bike.prototype
↓
Object.prototype
↓
null
So bike.ride() works even though ride() is defined in Bike.
14. super
super is used when I want to work with the parent class.
Example:
class Bike {
constructor(company, colour) {
this.company = company;
this.colour = colour;
}
}
class SuperBike extends Bike {
constructor(company, colour, speed) {
super(company, colour);
this.speed = speed;
}
}
This:
super(company, colour);
calls the parent constructor.
It is similar in purpose to:
Bike.call(this, company, colour);
in constructor-function inheritance.
One important rule:
In a derived class constructor,
super()must be called before usingthis.
15. Static Members
A normal method belongs to an instance:
class Bike {
show() {
console.log("Bike");
}
}
const bike = new Bike();
bike.show();
A static method belongs to the class itself:
class Bike {
static info() {
console.log("Bike information");
}
}
Bike.info();
I should not do:
const bike = new Bike();
bike.info(); // ❌
because info() is static.
Static property
class Bike {
static wheels = 2;
}
console.log(Bike.wheels);
The important difference is:
Normal method
↓
instance
Static method
↓
class itself
16. Static with Constructor Functions
I can also create a static-like member with a constructor function.
function Bike(company) {
this.company = company;
}
Bike.info = function () {
console.log("Bike information");
};
Bike.info();
Here info is attached directly to Bike.
I don't use:
const bike = new Bike("Yamaha");
bike.info(); // ❌
The main idea is:
Bike.info()
↓
belongs to Bike itself
bike.info()
↓
looks for an instance method
For modern JavaScript, I will usually see the static keyword with classes.
17. Getters
A getter lets me read a method like a property.
Without a getter:
class User {
constructor(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
getFullName() {
return `${this.firstName} ${this.lastName}`;
}
}
const user = new User("Koushik", "Maya");
console.log(user.getFullName());
I have to use:
user.getFullName();
With a getter:
class User {
constructor(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
get fullName() {
return `${this.firstName} ${this.lastName}`;
}
}
const user = new User("Koushik", "Maya");
console.log(user.fullName);
Now I use:
user.fullName
not:
user.fullName()
The simple meaning is:
get → when I READ the property, run this code
18. Getters and Setters — My Age Example
This is the example that helped me understand getters and setters.
class Person {
constructor(name, age) {
this.name = name;
this._age = age;
}
get age() {
return this._age;
}
set age(newAge) {
if (newAge < 0) {
console.log("Age cannot be negative");
return;
}
this._age = newAge;
}
}
const p1 = new Person("Koushik", 25);
console.log(p1.age);
p1.age = 26;
console.log(p1.age);
Output:
25
26
What is happening?
When I write:
console.log(p1.age);
I am reading age.
So the getter runs:
get age() {
return this._age;
}
It returns:
25
Then when I write:
p1.age = 26;
I am setting/changing age.
So the setter runs:
set age(newAge) {
Here:
newAge = 26
The condition:
if (newAge < 0)
is false.
So:
this._age = newAge;
changes the stored value to:
_age = 26
When I read it again:
console.log(p1.age);
the getter returns 26.
19. Why _age?
I use:
this._age
as the internal storage.
The _ is only a convention. It does not make the property private.
So this is still possible:
console.log(p1._age);
The idea is:
_age
↓
internal storage
age
↓
getter/setter interface
This also prevents a common problem.
If I wrote:
set age(newAge) {
this.age = newAge;
}
the setter would call itself again and again.
So instead I store the actual value in:
this._age
20. Setter = Validation and Controlled Changes
A setter becomes especially useful when I need to control what values are allowed.
For example:
class BankAccount {
constructor(balance) {
this._balance = balance;
}
get balance() {
return this._balance;
}
set balance(amount) {
if (amount < 0) {
console.log("Balance cannot be negative");
return;
}
this._balance = amount;
}
}
Now:
account.balance = 5000;
is allowed.
But:
account.balance = -5000;
is rejected.
So:
Getter
↓
Controls reading
Setter
↓
Controls changing
21. Encapsulation
Encapsulation means keeping internal implementation details controlled instead of exposing everything directly.
For example, this is very open:
class BankAccount {
constructor(balance) {
this.balance = balance;
}
}
Someone can do:
account.balance = 999999;
There is no validation.
JavaScript gives me different ways to keep internal state controlled.
The two important approaches here are:
- Closures
- Private class fields using
#
22. Encapsulation with Closures
A closure allows a function to remember variables from its outer scope.
Example:
function BankAccount(initialBalance) {
let balance = initialBalance;
return {
deposit(amount) {
if (amount <= 0) {
throw new Error("Invalid amount");
}
balance += amount;
},
withdraw(amount) {
if (amount > balance) {
throw new Error("Insufficient balance");
}
balance -= amount;
},
getBalance() {
return balance;
}
};
}
Create an account:
const account = BankAccount(1000);
Use it:
account.deposit(500);
console.log(account.getBalance());
Output:
1500
But:
console.log(account.balance);
returns:
undefined
Why?
Because balance is not a property of account.
It is a variable inside the BankAccount() function.
The returned functions remember it.
Conceptually:
BankAccount()
│
├── balance = 1000 ← hidden
│
├── deposit()
├── withdraw()
└── getBalance()
│
└── can access balance
This is closure-based encapsulation.
23. Private Fields with #
Modern JavaScript gives us actual private class fields.
class BankAccount {
#balance;
constructor(initialBalance) {
this.#balance = initialBalance;
}
deposit(amount) {
this.#balance += amount;
}
getBalance() {
return this.#balance;
}
}
Use it:
const account = new BankAccount(1000);
account.deposit(500);
console.log(account.getBalance());
Output:
1500
But this is not allowed:
console.log(account.#balance);
#balance is actually private to the class.
24. _field vs #field
This distinction is very important.
this._balance
means:
"I intend this to be an internal property."
But JavaScript does not enforce that.
Someone can still do:
account._balance;
Whereas:
this.#balance
is a real private field.
So:
_balance
↓
Convention only
#balance
↓
Actual JavaScript private field
25. Private Methods
Methods can also be private.
class BankAccount {
#balance = 1000;
#validateAmount(amount) {
return amount > 0;
}
deposit(amount) {
if (!this.#validateAmount(amount)) {
throw new Error("Invalid amount");
}
this.#balance += amount;
}
}
This method:
#validateAmount()
can only be used inside the class.
Outside code cannot call:
account.#validateAmount(100);
26. Closures vs Private Fields
Both can hide internal state.
Closure
function Counter() {
let count = 0;
return {
increment() {
count++;
},
getCount() {
return count;
}
};
}
Here count is hidden inside the function scope.
Private field
class Counter {
#count = 0;
increment() {
this.#count++;
}
getCount() {
return this.#count;
}
}
Here #count is private because JavaScript enforces the private-field rule.
My simple distinction:
Closure
↓
private variable in lexical scope
#field
↓
JavaScript-enforced private class field
27. One Complete Example
Now I can combine the concepts:
- Class
- Inheritance
static- Getter
- Setter
- Private field
class Person {
static species = "Human";
#age;
constructor(name, age) {
this.name = name;
this.#age = age;
}
get age() {
return this.#age;
}
set age(value) {
if (value < 0) {
throw new Error("Age cannot be negative");
}
this.#age = value;
}
introduce() {
console.log(
`My name is ${this.name} and I am ${this.#age} years old.`
);
}
}
class Student extends Person {
constructor(name, age, course) {
super(name, age);
this.course = course;
}
study() {
console.log(
`${this.name} is studying ${this.course}`
);
}
}
const student = new Student(
"Koushik",
22,
"JavaScript"
);
student.introduce();
student.study();
console.log(student.age);
student.age = 23;
console.log(student.age);
console.log(Person.species);
Output:
My name is Koushik and I am 22 years old.
Koushik is studying JavaScript
22
23
Human
28. Understanding the Prototype Chain in This Example
When I write:
const student = new Student(
"Koushik",
22,
"JavaScript"
);
the object is connected to prototypes roughly like this:
student
↓
Student.prototype
↓
Person.prototype
↓
Object.prototype
↓
null
If I call:
student.study();
JavaScript finds study() on:
Student.prototype
If I call:
student.introduce();
JavaScript searches:
student
↓
Student.prototype
↓
Person.prototype ← introduce() found
This is what is happening underneath class inheritance.
29. Where Does #age Fit?
The private field is used inside the getter and setter.
Getter:
get age() {
return this.#age;
}
So:
console.log(student.age);
flows like:
student.age
↓
get age()
↓
this.#age
↓
private value
When I write:
student.age = 23;
the setter runs:
student.age = 23
↓
set age(value)
↓
validate value
↓
this.#age = 23
So the getter/setter gives me controlled access to the private field.
30. Prototype Methods vs Instance Properties
This is another important distinction.
Consider:
class User {
constructor(name) {
this.name = name;
}
greet() {
console.log(`Hello ${this.name}`);
}
}
Each object has its own:
name
But greet() is shared through:
User.prototype
Conceptually:
user1 ─────┐
│
user2 ─────┼──→ User.prototype → greet()
│
user3 ─────┘
This is one reason prototypes are important.
The method does not need to be separately recreated on every instance.
31. My Final Mental Model
This is how I now connect the topics:
JavaScript Objects
│
▼
Prototype System
│
┌────────────┴────────────┐
▼ ▼
Prototype Chain Object.create()
│
▼
Classes
│
┌─────┴─────┐
▼ ▼
extends static
│
▼
super
For controlling internal state:
Encapsulation
│
┌───────┴────────┐
▼ ▼
Closures #private
│ │
└───────┬────────┘
▼
Controlled Data
│
┌──────┴──────┐
▼ ▼
getter setter
32. The Main Things I Need to Remember
Prototype
An object can look at another object for properties/methods.
Prototype chain
JavaScript searches from the object upward until it finds the property.
Object.create()
const child = Object.create(parent);
Creates an object whose prototype is parent.
Constructor function
function Bike(...) {}
Acts like a blueprint for creating objects with new.
Constructor + prototype
Bike.prototype.method = function () {};
Stores a shared method on the prototype.
Constructor inheritance
Child.prototype = Object.create(Parent.prototype);
Creates the parent-child prototype relationship.
class
class Bike {}
Provides cleaner syntax while still using prototypes underneath.
extends
class SuperBike extends Bike {}
Creates class inheritance.
super
super(...);
Accesses the parent constructor or parent method.
static
static info() {}
Belongs to the class itself, not its instances.
Getter
get age() {}
Runs when I read:
person.age
Setter
set age(value) {}
Runs when I write:
person.age = value;
_field
_age
Convention only. Not truly private.
#field
#age
Actual JavaScript private field.
Closure
A function can remember variables from its outer scope.
Conclusion
The biggest thing I learned is that these features are not random, separate JavaScript concepts.
The prototype system is the foundation.
Object
↓
Prototype
↓
Prototype Chain
↓
Inheritance
↓
Classes
And for controlling internal state:
Encapsulation
├── Closures
└── Private fields (#field)
Getters and setters can then provide a controlled way to access that state:
getter → read
setter → change
The most important idea for me is:
JavaScript classes do not replace prototypes. Classes provide cleaner syntax for working with JavaScript's prototype-based object system.
And the privacy rule I want to remember is:
_fieldis a convention, while#fieldprovides actual private class fields.
If I understand the prototype chain first, the rest of JavaScript's object-oriented features become much easier to understand.
Top comments (0)