Module 1: Classes & OOP
Classes & Constructors
A class is a blueprint for creating objects that share the same structure and behavior:
class User {
constructor(name, age) { // runs automatically when a new object is created
this.name = name;
this.age = age;
}
greet() {
console.log(`Hi, I'm ${this.name}`);
}
}
let user1 = new User("Chiru", 25);
user1.greet(); // "Hi, I'm Chiru"
Inheritance
One class can extend another, reusing and building on its behavior — this is encapsulation (bundling data and behavior together) combined with inheritance (sharing that bundle across related classes):
class Admin extends User {
constructor(name, age, permissions) {
super(name, age); // calls User's constructor first
this.permissions = permissions;
}
greet() {
super.greet(); // calls User's greet() first
console.log("I'm an admin");
}
}
let admin = new Admin("Ankur", 30, ["manage-users"]);
admin.greet();
// "Hi, I'm Ankur"
// "I'm an admin"
This is also polymorphism in action — Admin and User
both have a greet() method, but each runs its own version, and code that expects a
User can work with an Admin too.
this, call, apply & bind
Inside a method, this refers to the object the method was called on:
let counter = {
count: 0,
increment() {
this.count++;
}
};
counter.increment();
call, apply, and bind let you explicitly control what
this refers to:
function greet() {
console.log(`Hi, I'm ${this.name}`);
}
let user1 = { name: "Chiru" };
greet.call(user1); // runs immediately, this = user1
greet.apply(user1, []); // like call, but arguments as an array
let boundGreet = greet.bind(user1); // returns a NEW function, permanently locked to user1
boundGreet(); // can be called anytime later, this never changes
bind is especially useful when passing a method as a callback, where it would
otherwise lose track of its original this.
Prototypes & the Prototype Chain
Under the hood, JavaScript classes are built on prototypes — every object has a
hidden link to another object (its prototype), which it can borrow properties and methods from.
When you access user1.greet(), JavaScript looks for greet on
user1 itself first, and if not found, walks up the prototype chain until it finds it
(or reaches the end and gives up).
class User {
greet() { console.log("Hi"); }
}
let user1 = new User();
Object.getPrototypeOf(user1) === User.prototype; // true
user1.hasOwnProperty("greet"); // false — greet lives on the prototype, not on user1 itself
You rarely need to work with prototypes directly in modern code — classes give you the same power with much clearer syntax — but understanding they exist under the hood explains a lot of JavaScript's behavior.
Getters & Setters
Let you define properties that run code when read or written, while still looking like plain property access from the outside:
class Circle {
constructor(radius) {
this._radius = radius;
}
get area() {
return Math.PI * this._radius ** 2;
}
set radius(value) {
if (value <= 0) throw new Error("Radius must be positive");
this._radius = value;
}
}
let c = new Circle(5);
console.log(c.area); // called like a property, not a method — no ()
c.radius = 10; // also looks like a plain assignment
console.log(c.area);