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();
Enter fullscreen mode Exit fullscreen mode
Output:
Barking
Eating
Enter fullscreen mode Exit fullscreen mode
The important thing is that eat() is not actually inside dog.
JavaScript finds it through dog‘s prototype.
dog
↓
animal
↓
Object.prototype
↓
null
Enter fullscreen mode Exit fullscreen mode
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());
Enter fullscreen mode Exit fullscreen mode
I never created toString() inside person.
So how does it work?
JavaScript searches:
person
↓
Object.prototype
↓
null
Enter fullscreen mode Exit fullscreen mode
toString() is found on Object.prototype.
Property lookup in simple steps
When I write:
object.someProperty
Enter fullscreen mode Exit fullscreen mode
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();
Enter fullscreen mode Exit fullscreen mode
The search is:
dog
↓
animal ← eat() found here
↓
Object.prototype
↓
null
Enter fullscreen mode Exit fullscreen mode
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();
Enter fullscreen mode Exit fullscreen mode
The object created by new Person() can find sayHello() through:
Person.prototype
Enter fullscreen mode Exit fullscreen mode
The chain is:
person
↓
Person.prototype
↓
Object.prototype
↓
null
Enter fullscreen mode Exit fullscreen mode
__proto__
__proto__ gives access to an object’s actual prototype.
console.log(person.__proto__ === Person.prototype);
Enter fullscreen mode Exit fullscreen mode
Output:
true
Enter fullscreen mode Exit fullscreen mode
For modern code, I should prefer:
Object.getPrototypeOf(person);
Enter fullscreen mode Exit fullscreen mode
instead of directly using __proto__.
4. Object.create()
Object.create() creates a new object and lets me choose its prototype.
Syntax:
Object.create(prototype);
Enter fullscreen mode Exit fullscreen mode
Example:
const animal = {
eat() {
console.log("Eating");
}
};
const dog = Object.create(animal);
dog.bark = function () {
console.log("Barking");
};
dog.eat();
dog.bark();
Enter fullscreen mode Exit fullscreen mode
The relationship is:
dog
↓
animal
Enter fullscreen mode Exit fullscreen mode
This is the simplest form of object-to-object inheritance.
My simple way of remembering it
const child = Object.create(parent);
Enter fullscreen mode Exit fullscreen mode
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();
Enter fullscreen mode Exit fullscreen mode
Here:
obj2
↓
obj
Enter fullscreen mode Exit fullscreen mode
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;
}
Enter fullscreen mode Exit fullscreen mode
I can think of this as a blueprint.
Then:
const bike1 = new Bike("Yamaha", "Blue", 1000);
Enter fullscreen mode Exit fullscreen mode
creates an object.
Conceptually:
bike1
├── company → "Yamaha"
├── colour → "Blue"
└── cc → 1000
Enter fullscreen mode Exit fullscreen mode
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();
Enter fullscreen mode Exit fullscreen mode
The important thing is that fun() is on:
Bike.prototype
Enter fullscreen mode Exit fullscreen mode
not copied directly into every object.
The chain is:
bike1
↓
Bike.prototype
↓
Object.prototype
↓
null
Enter fullscreen mode Exit fullscreen mode
When I call:
bike1.fun();
Enter fullscreen mode Exit fullscreen mode
JavaScript looks for fun().
It checks:
bike1 → not found
Bike.prototype → found
Enter fullscreen mode Exit fullscreen mode
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);
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
Actual constructor-function inheritance needs a second constructor.
9. Constructor Function + Prototype Inheritance
Now I can make:
Bike
↓
SuperBike
Enter fullscreen mode Exit fullscreen mode
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();
Enter fullscreen mode Exit fullscreen mode
Where does inheritance actually happen?
This line:
SuperBike.prototype = Object.create(Bike.prototype);
Enter fullscreen mode Exit fullscreen mode
is the important inheritance line.
It creates:
bike1
↓
SuperBike.prototype
↓
Bike.prototype
↓
Object.prototype
↓
null
Enter fullscreen mode Exit fullscreen mode
So:
bike1.fun();
Enter fullscreen mode Exit fullscreen mode
works because JavaScript eventually finds fun() in Bike.prototype.
10. Why Bike.call()?
This line:
Bike.call(this, company, colour, cc);
Enter fullscreen mode Exit fullscreen mode
handles the parent’s properties.
The parent constructor has:
function Bike(company, colour, cc) {
this.company = company;
this.colour = colour;
this.cc = cc;
}
Enter fullscreen mode Exit fullscreen mode
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;
Enter fullscreen mode Exit fullscreen mode
Then SuperBike adds:
this.topspeed = topspeed;
Enter fullscreen mode Exit fullscreen mode
So I remember the pattern like this:
Bike.call(...)
↓
Get parent's properties
Object.create(Bike.prototype)
↓
Get parent's prototype methods
Enter fullscreen mode Exit fullscreen mode
11. Object.create() vs Constructor Inheritance
This was one of the things I wanted to understand clearly.
Object.create()
const child = Object.create(parent);
Enter fullscreen mode Exit fullscreen mode
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();
Enter fullscreen mode Exit fullscreen mode
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);
Enter fullscreen mode Exit fullscreen mode
Now I have:
Child
↓
Parent
Enter fullscreen mode Exit fullscreen mode
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();
Enter fullscreen mode Exit fullscreen mode
This looks very different from:
function Bike(company, colour, cc) {
this.company = company;
this.colour = colour;
this.cc = cc;
}
Bike.prototype.fun = function () {
// ...
};
Enter fullscreen mode Exit fullscreen mode
But the important thing is:
Classes still use prototypes underneath.
The class method is placed on:
Bike.prototype
Enter fullscreen mode Exit fullscreen mode
rather than copied separately into every instance.
So:
class syntax
↓
prototype system underneath
Enter fullscreen mode Exit fullscreen mode
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();
Enter fullscreen mode Exit fullscreen mode
The inheritance chain is approximately:
bike
↓
SuperBike.prototype
↓
Bike.prototype
↓
Object.prototype
↓
null
Enter fullscreen mode Exit fullscreen mode
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;
}
}
Enter fullscreen mode Exit fullscreen mode
This:
super(company, colour);
Enter fullscreen mode Exit fullscreen mode
calls the parent constructor.
It is similar in purpose to:
Bike.call(this, company, colour);
Enter fullscreen mode Exit fullscreen mode
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();
Enter fullscreen mode Exit fullscreen mode
A static method belongs to the class itself:
class Bike {
static info() {
console.log("Bike information");
}
}
Bike.info();
Enter fullscreen mode Exit fullscreen mode
I should not do:
const bike = new Bike();
bike.info(); // ❌
Enter fullscreen mode Exit fullscreen mode
because info() is static.
Static property
class Bike {
static wheels = 2;
}
console.log(Bike.wheels);
Enter fullscreen mode Exit fullscreen mode
The important difference is:
Normal method
↓
instance
Static method
↓
class itself
Enter fullscreen mode Exit fullscreen mode
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();
Enter fullscreen mode Exit fullscreen mode
Here info is attached directly to Bike.
I don’t use:
const bike = new Bike("Yamaha");
bike.info(); // ❌
Enter fullscreen mode Exit fullscreen mode
The main idea is:
Bike.info()
↓
belongs to Bike itself
bike.info()
↓
looks for an instance method
Enter fullscreen mode Exit fullscreen mode
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());
Enter fullscreen mode Exit fullscreen mode
I have to use:
user.getFullName();
Enter fullscreen mode Exit fullscreen mode
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);
Enter fullscreen mode Exit fullscreen mode
Now I use:
user.fullName
Enter fullscreen mode Exit fullscreen mode
not:
user.fullName()
Enter fullscreen mode Exit fullscreen mode
The simple meaning is:
get → when I READ the property, run this code
Enter fullscreen mode Exit fullscreen mode
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);
Enter fullscreen mode Exit fullscreen mode
Output:
25
26
Enter fullscreen mode Exit fullscreen mode
What is happening?
When I write:
console.log(p1.age);
Enter fullscreen mode Exit fullscreen mode
I am reading age.
So the getter runs:
get age() {
return this._age;
}
Enter fullscreen mode Exit fullscreen mode
It returns:
25
Enter fullscreen mode Exit fullscreen mode
Then when I write:
p1.age = 26;
Enter fullscreen mode Exit fullscreen mode
I am setting/changing age.
So the setter runs:
set age(newAge) {
Enter fullscreen mode Exit fullscreen mode
Here:
newAge = 26
Enter fullscreen mode Exit fullscreen mode
The condition:
if (newAge < 0)
Enter fullscreen mode Exit fullscreen mode
is false.
So:
this._age = newAge;
Enter fullscreen mode Exit fullscreen mode
changes the stored value to:
_age = 26
Enter fullscreen mode Exit fullscreen mode
When I read it again:
console.log(p1.age);
Enter fullscreen mode Exit fullscreen mode
the getter returns 26.
19. Why _age?
I use:
this._age
Enter fullscreen mode Exit fullscreen mode
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);
Enter fullscreen mode Exit fullscreen mode
The idea is:
_age
↓
internal storage
age
↓
getter/setter interface
Enter fullscreen mode Exit fullscreen mode
This also prevents a common problem.
If I wrote:
set age(newAge) {
this.age = newAge;
}
Enter fullscreen mode Exit fullscreen mode
the setter would call itself again and again.
So instead I store the actual value in:
this._age
Enter fullscreen mode Exit fullscreen mode
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;
}
}
Enter fullscreen mode Exit fullscreen mode
Now:
account.balance = 5000;
Enter fullscreen mode Exit fullscreen mode
is allowed.
But:
account.balance = -5000;
Enter fullscreen mode Exit fullscreen mode
is rejected.
So:
Getter
↓
Controls reading
Setter
↓
Controls changing
Enter fullscreen mode Exit fullscreen mode
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;
}
}
Enter fullscreen mode Exit fullscreen mode
Someone can do:
account.balance = 999999;
Enter fullscreen mode Exit fullscreen mode
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;
}
};
}
Enter fullscreen mode Exit fullscreen mode
Create an account:
const account = BankAccount(1000);
Enter fullscreen mode Exit fullscreen mode
Use it:
account.deposit(500);
console.log(account.getBalance());
Enter fullscreen mode Exit fullscreen mode
Output:
1500
Enter fullscreen mode Exit fullscreen mode
But:
console.log(account.balance);
Enter fullscreen mode Exit fullscreen mode
returns:
undefined
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
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;
}
}
Enter fullscreen mode Exit fullscreen mode
Use it:
const account = new BankAccount(1000);
account.deposit(500);
console.log(account.getBalance());
Enter fullscreen mode Exit fullscreen mode
Output:
1500
Enter fullscreen mode Exit fullscreen mode
But this is not allowed:
console.log(account.#balance);
Enter fullscreen mode Exit fullscreen mode
#balance is actually private to the class.
24. _field vs #field
This distinction is very important.
this._balance
Enter fullscreen mode Exit fullscreen mode
means:
“I intend this to be an internal property.”
But JavaScript does not enforce that.
Someone can still do:
account._balance;
Enter fullscreen mode Exit fullscreen mode
Whereas:
this.#balance
Enter fullscreen mode Exit fullscreen mode
is a real private field.
So:
_balance
↓
Convention only
#balance
↓
Actual JavaScript private field
Enter fullscreen mode Exit fullscreen mode
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;
}
}
Enter fullscreen mode Exit fullscreen mode
This method:
#validateAmount()
Enter fullscreen mode Exit fullscreen mode
can only be used inside the class.
Outside code cannot call:
account.#validateAmount(100);
Enter fullscreen mode Exit fullscreen mode
26. Closures vs Private Fields
Both can hide internal state.
Closure
function Counter() {
let count = 0;
return {
increment() {
count++;
},
getCount() {
return count;
}
};
}
Enter fullscreen mode Exit fullscreen mode
Here count is hidden inside the function scope.
Private field
class Counter {
#count = 0;
increment() {
this.#count++;
}
getCount() {
return this.#count;
}
}
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
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);
Enter fullscreen mode Exit fullscreen mode
Output:
My name is Koushik and I am 22 years old.
Koushik is studying JavaScript
22
23
Human
Enter fullscreen mode Exit fullscreen mode
28. Understanding the Prototype Chain in This Example
When I write:
const student = new Student(
"Koushik",
22,
"JavaScript"
);
Enter fullscreen mode Exit fullscreen mode
the object is connected to prototypes roughly like this:
student
↓
Student.prototype
↓
Person.prototype
↓
Object.prototype
↓
null
Enter fullscreen mode Exit fullscreen mode
If I call:
student.study();
Enter fullscreen mode Exit fullscreen mode
JavaScript finds study() on:
Student.prototype
Enter fullscreen mode Exit fullscreen mode
If I call:
student.introduce();
Enter fullscreen mode Exit fullscreen mode
JavaScript searches:
student
↓
Student.prototype
↓
Person.prototype ← introduce() found
Enter fullscreen mode Exit fullscreen mode
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;
}
Enter fullscreen mode Exit fullscreen mode
So:
console.log(student.age);
Enter fullscreen mode Exit fullscreen mode
flows like:
student.age
↓
get age()
↓
this.#age
↓
private value
Enter fullscreen mode Exit fullscreen mode
When I write:
student.age = 23;
Enter fullscreen mode Exit fullscreen mode
the setter runs:
student.age = 23
↓
set age(value)
↓
validate value
↓
this.#age = 23
Enter fullscreen mode Exit fullscreen mode
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}`);
}
}
Enter fullscreen mode Exit fullscreen mode
Each object has its own:
name
Enter fullscreen mode Exit fullscreen mode
But greet() is shared through:
User.prototype
Enter fullscreen mode Exit fullscreen mode
Conceptually:
user1 ─────┐
│
user2 ─────┼──→ User.prototype → greet()
│
user3 ─────┘
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
For controlling internal state:
Encapsulation
│
┌───────┴────────┐
▼ ▼
Closures #private
│ │
└───────┬────────┘
▼
Controlled Data
│
┌──────┴──────┐
▼ ▼
getter setter
Enter fullscreen mode Exit fullscreen mode
32. The Main Things I Need to Remember
Prototype
An object can look at another object for properties/methods.
Enter fullscreen mode Exit fullscreen mode
Prototype chain
JavaScript searches from the object upward until it finds the property.
Enter fullscreen mode Exit fullscreen mode
Object.create()
const child = Object.create(parent);
Enter fullscreen mode Exit fullscreen mode
Creates an object whose prototype is parent.
Constructor function
function Bike(...) {}
Enter fullscreen mode Exit fullscreen mode
Acts like a blueprint for creating objects with new.
Constructor + prototype
Bike.prototype.method = function () {};
Enter fullscreen mode Exit fullscreen mode
Stores a shared method on the prototype.
Constructor inheritance
Child.prototype = Object.create(Parent.prototype);
Enter fullscreen mode Exit fullscreen mode
Creates the parent-child prototype relationship.
class
class Bike {}
Enter fullscreen mode Exit fullscreen mode
Provides cleaner syntax while still using prototypes underneath.
extends
class SuperBike extends Bike {}
Enter fullscreen mode Exit fullscreen mode
Creates class inheritance.
super
super(...);
Enter fullscreen mode Exit fullscreen mode
Accesses the parent constructor or parent method.
static
static info() {}
Enter fullscreen mode Exit fullscreen mode
Belongs to the class itself, not its instances.
Getter
get age() {}
Enter fullscreen mode Exit fullscreen mode
Runs when I read:
person.age
Enter fullscreen mode Exit fullscreen mode
Setter
set age(value) {}
Enter fullscreen mode Exit fullscreen mode
Runs when I write:
person.age = value;
Enter fullscreen mode Exit fullscreen mode
_field
_age
Enter fullscreen mode Exit fullscreen mode
Convention only. Not truly private.
#field
#age
Enter fullscreen mode Exit fullscreen mode
Actual JavaScript private field.
Closure
A function can remember variables from its outer scope.
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
And for controlling internal state:
Encapsulation
├── Closures
└── Private fields (#field)
Enter fullscreen mode Exit fullscreen mode
Getters and setters can then provide a controlled way to access that state:
getter → read
setter → change
Enter fullscreen mode Exit fullscreen mode
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.