Skip to content

Prototypes and Object-Oriented Programming

Object-oriented programming (OOP) is a way of organizing code using objects, where each object holds both data and behavior together.

In JavaScript, OOP is prototype-based, not class-based like some other languages.

Even when you use the class keyword, JavaScript is still secretly using prototype chains behind the scenes.

Reuse

You write the logic once, and reuse it everywhere you need it.

Structure

It helps you organize your code in a clean way.

Scalability

It becomes easier to grow and extend large systems.

Performance

Sharing prototype methods saves memory.
graph TD A[Instance] --> B[Prototype] B --> C[Prototype] C --> D[null]
  1. JavaScript first checks the object itself.
  2. If the property is not found there -> it checks the prototype.
  3. It keeps moving upward through the chain like this.
  4. It finally stops once it reaches null.
const animal = {
speak() {
return "Some sound";
},
};
const dog = Object.create(animal);
dog.name = "Bruno";
console.log(dog.speak());
graph LR A[dog] -->|no speak| B[animal] B -->|found| C[execute]
const baseUser = { role: "user" };
const admin = Object.create(baseUser);
admin.name = "Riya";
console.log(admin.name); // own
console.log(admin.role); // prototype
admin.role = "admin";

Now:

console.log(admin.role); // admin

This is called shadowing, it simply means that admin now has its own role property, and this new one “hides” the one coming from the prototype.

__proto__

This is the actual link to the parent object, while your code is running.

prototype

This is a property that belongs to a constructor function.

[[Prototype]]

This is an internal slot used by the JavaScript engine itself.

function Person() {}
const p = new Person();
p.__proto__ === Person.prototype; // true
function Person(name) {
this.name = name;
}
Person.prototype.greet = function () {
return `Hi ${this.name}`;
};
graph TD A[p1] --> B[Person.prototype] C[p2] --> B
function User(name) {
this.name = name;
}
User.prototype.role = "user";
const u1 = new User("A");
const u2 = new User("B");
u1.role = "admin";
console.log(u1.role); // admin
console.log(u2.role); // user
  • u1.role -> here, a new property is created directly on u1 itself.
  • u2.role -> this one is still coming from the shared prototype.
const user = {
first: "Sahil",
last: "Kumar",
get fullName() {
return `${this.first} ${this.last}`;
},
set fullName(value) {
[this.first, this.last] = value.split(" ");
},
};

A getter lets you read a value as if it were a normal property, but it actually runs a small function behind the scenes. A setter does the same thing, but for writing (setting) a value.

console.log(user.fullName);
user.fullName = "John Doe";
function Person(name) {
this.name = name;
}
Person.prototype = {
get display() {
return `Name: ${this.name}`;
},
};
const obj = { a: 1 };
obj.hasOwnProperty("a"); // true
"a" in obj; // true
  • hasOwnProperty -> this only checks the object’s own properties.
  • in -> this also checks properties coming from the prototype chain.
class Vehicle {
constructor(brand) {
this.brand = brand;
}
start() {
return `${this.brand} started`;
}
}
Vehicle.prototype.start = function () {};

Even though this looks like a normal class, JavaScript is actually still attaching the start method onto the prototype behind the scenes, just like before.

class Animal {
speak() {
return "sound";
}
}
class Dog extends Animal {
bark() {
return "woof";
}
}
graph TD A[Dog] --> B[Animal] B --> C[Object]
  1. Encapsulation -> this means bundling data and methods together inside one place.
  2. Inheritance -> this means reusing logic from another object or class.
  3. Polymorphism -> this means the same method name can behave differently depending on the object.
  4. Abstraction -> this means hiding away the complicated details that you do not need to see.
class BankAccount {
#balance = 0;
deposit(a) {
this.#balance += a;
}
getBalance() {
return this.#balance;
}
}

Here, #balance is a private field. This means it can only be accessed from inside the class itself, not from outside it.

class Shape {
area() {
return 0;
}
}
User.prototype.say = function () {
return this.name;
};
const obj1 = Object.create(proto);
const obj2 = new Constructor();

Object.create lets you directly set the prototype of a new object. new instead calls a constructor function to build the object for you.

Object.setPrototypeOf(obj, proto);
function Test() {}
Test.prototype.arr = [];
const a = new Test();
const b = new Test();
a.arr.push(1);
console.log(b.arr); // [1]

This happens because arr lives on the shared prototype, not on each individual object. So when a changes it, b sees the change too, since they are both looking at the exact same array.

class ApiClient {
async get() {
return fetch("/api").then((r) => r.json());
}
}