What is Object-Oriented Programming (OOP)?
Object-Oriented Programming is a way of writing code that centers on the idea of objects. These objects can be thought of as building blocks to create things.
In OOP, you create these objects based on a blueprint known as a class.
Think of it like designing cars in a factory:
- You have a blueprint (called a class).
- You use that blueprint to create multiple cars (objects).
- Each car has the same structure, but can have different details (color, speed, brand).
OOP helps you keep your code organized and makes it easier to understand how different parts of your program interact with each other.
Core Principles of OOP
Object-Oriented Programming is based on four main principles:
- Encapsulation: Keeping data (properties) and functions (methods) together inside a class and restricting direct access to some of the object’s components to protect the data.
- Inheritance: Allowing a class to inherit properties and methods from another class to promote code reuse.
- Polymorphism: The ability for different classes to be treated as instances of the same class through a common interface.
- Abstraction: Hiding complex implementation details and showing only the necessary parts.
Why use OOP?
- Keeps code clean and organized
- Makes it easier to reuse logic (no copy-pasting!)
- Helps you model real-world things in code
- Makes collaboration and scaling easier
- Enables encapsulation to protect data and group related functionality together
- Supports inheritance so you can create new classes based on existing ones without rewriting code
Classes and Constructors
In JavaScript, a class is like a blueprint, it defines what an object should look like and how it should behave.
The constructor is a special function inside the class. It runs automatically when you create a new object and sets up the initial values (just like assembling a new car using a template).
For example:
class Car {
constructor(company, model) {
this.company = company;
this.model = model;
}
}
Here,
class Car
is the blueprint for making car objects.constructor(company, model)
is like the instruction manual, it tells JavaScript how to build each car.this.company
andthis.model
are properties assigned to each object (like setting the brand and model of the car).
So whenever you say new Car("Toyota", "Camry")
, JavaScript uses the class
and constructor
to build a brand-new object with those details.
Creating Objects
Once you've defined a class
(blueprint), you can build objects (instances) from it using the new
keyword.
Example:
const carObject = new Car("Ford", "Ecosport");
Here,
-
new Car(...)
tells JavaScript: "Use the Car blueprint and run the constructor." -
"Ford" and "Ecosport" are passed into the constructor.
-
A brand-new object is created with these values assigned.
-
When you do
new Car("Ford", "Ecosport")
, it creates an object like:{ company: "Ford", model: "Ecosport" }
Now you have a carObject
that holds details about a specific car, ready to use in your program.
Prototype
In JavaScript, prototype
is how objects share methods and properties.
When you create a new object using a class or constructor function, JavaScript automatically links it to its prototype
. This means your object can access any method or property defined on that prototype
, even if the object doesn’t define it directly.
When you try to access a property or method on an object, JavaScript first checks if that property or method exists on the object itself. If it doesn't, JavaScript looks up the prototype
object. If the prototype
doesn't have it, JavaScript keeps looking at "parent" prototypes until it finds the property or method.
To put it simply, think of it as borrowing things from a friend. If you don't have something, you ask your friend. If your friend doesn't have it, they might ask their friend, and this can continue until someone has what you're looking for.
For example:
class Car {
constructor(company, model) {
this.company = company;
this.model = model;
}
}
// Add a method to the Car prototype
Car.prototype.hello = function () {
console.log(`Hello, I'm ${this.model} model of ${this.company} company!`);
};
// Create car objects
const carObject1 = new Car("Ford", "Ecosport");
const carObject2 = new Car("Toyota", "Fortuner");
carObject1.hello(); // Output: Hello, I'm Ecosport model of Ford company!
carObject2.hello(); // Output: Hello, I'm Fortuner model of Toyota company!
Here,
class Car { ... }
: This defines a blueprint for creating car objects.constructor(company, model)
: A special method that runs when you create a new object usingnew Car(...)
.this.company = company
: Sets thecompany
property on the new object.this.model = model
: Sets themodel
property on the new object.Car.prototype.hello = function () {...};
:- Adds a method
hello()
to the Car prototype. - All objects created using
new Car(...)
can access this method via theprototype
chain. - Inside the function,
this.model
andthis.company
refer to the properties of the object calling the method.
- Adds a method
const carObject1 = new Car("Ford", "Ecosport");
carObject1 is an object representing a Ford Ecosport.const carObject1 = new Car("Toyota", "Fortuner");
carObject1 is an object representing a Toyota Fortuner.- Both are instances of the Car class.
- When you call
carObject1.hello()
, JavaScript looks forhello()
oncarObject1
and doesn’t find it directly. So it looks atCar.prototype
, findshello
, and runs it with this pointing to carObject1. - Same for
carObject2
.
When you define methods inside a class, they are automatically added to the class's prototype
behind the scenes.
For example:
// class and constructor
class Car {
constructor(company, model) {
this.company = company;
this.model = model;
}
hello() {
// Add a method to the Car prototype
console.log(`Hello, I'm ${this.model} model of ${this.company} company!`);
}
}
// Creating objects using the Car constructor
const carObject1 = new Car("Ford", "Ecosport");
const carObject2 = new Car("Toyota", "Fortuner");
carObject1.hello(); // Output: Hello, I'm Ecosport model of Ford company!
carObject2.hello(); // Output: Hello, I'm Fortuner model of Toyota company!
This is just a cleaner syntax. Internally, it’s doing the same thing as Car.prototype.hello = ...
.
Note: While you can add methods directly to a class’s prototype (like Car.prototype.hello = ...
), the modern and preferred way is to define methods inside the class body. This keeps your code cleaner and easier to read.
this keyword
In JavaScript, this
refers to the object that's currently executing the code.
Inside a class, when you write this.name
, you're referring to the name
property of that specific object (instance).
Learn more about this keyword!
class Person {
constructor(name) {
this.name = name;
}
hello() {
console.log(`Hello, I'm ${this.name}!`);
}
}
const person1 = new Person("John");
const person2 = new Person("Mark");
person1.hello(); // Output: Hello, I'm John!
person2.hello(); // Output: Hello, I'm Mark!
Here,
constructor(name)
: Setsthis.name
to the value passed when creating a new Person.hello()
: A method that printsthis.name
, referring to the object calling the method.- Creating Instances:
const person1 = new Person("John"); const person2 = new Person("Mark");
- You now have:
person1 = { name: "John", hello: [Function] }; person2 = { name: "Mark", hello: [Function] };
- Each Person has its own
name
, but shares the samehello()
method. person1.hello()
refers toperson1
, so it prints "Hello, I'm John!".person2.hello()
refers toperson2
, so it prints "Hello, I'm Mark!".
Without this
, you’d have to hardcode or duplicate logic per object. this
makes methods reusable and object-specific.
Class Inheritance
Inheritance is a key feature of OOP that lets a new class (child) inherit properties and methods from an existing class (parent). This helps you reuse code and build on existing functionality without rewriting it.
For example, you can create a base Car
class and then extend it to make a CarYear
class that includes a year
property.
// Parent class
class Car {
constructor(company, model) {
this.company = company;
this.model = model;
}
}
// Child class
class CarYear extends Car {
constructor(company, model, year) {
// Call parent constructor with company and model
super(company, model);
// Add new property specific to CarYear
this.year = year;
}
}
// Create a new car
const carObject = new CarYear("Ford", "Ecosport", 2013);
console.log(carObject.company); // Output: Ford
console.log(carObject.model); // Output: Ecosport
console.log(carObject.year); // Output: 2013
Here,
Car
is the parent class. It definescompany
andmodel
.CarYear
is the child class. It inherits fromCar
using extends.- Inside
CarYear
, you callsuper(company, model)
to run theCar
constructor. - Then you add an extra year property specific to
CarYear
.
What is Super?
The super
keyword is used to call and access methods or properties from a parent class within a child class.
You can also call methods from the parent using super.methodName()
.
Method Overriding
Method overriding means redefining a method in a child class that already exists in the parent class.
The method name stays the same, but its behavior changes in the child.
This is useful when the child class needs a more specific or updated version of the parent’s method.
For example:
// Parent class
class Car {
constructor(company, model) {
this.company = company;
this.model = model;
}
hello() {
console.log(`Hello, I'm ${this.model} model of ${this.company} company!`);
}
}
// Child class
class CarYear extends Car {
constructor(company, model, year) {
super(company, model);
this.year = year;
}
// Overriding the hello method
hello() {
console.log(
`Hello, I'm ${this.year} ${this.model} model of ${this.company} company!`
);
}
}
// Creating object using the Car constructor
const carObject = new Car("Ford", "Ecosport");
// Creating object using the CarYear constructor
const CarYearObject = new CarYear("Ford", "Ecosport", 2013);
carObject.hello(); // Output: Hello, I'm Ecosport model of Ford company!
CarYearObject.hello(); // Output: Hello, I'm 2013 Ecosport model of Ford company!
Here,
- You have a
Car
class with a methodhello()
and aCarYear
class that extends Car. - In the
CarYear
class, you override thehello()
method. - When you create an object of the
CarYear
class and call thehello()
method, it uses the overridden method from theCarYear
class, not the one from theCar
class. - This is method overriding.
Note: If needed, the child class can still access the parent method using super.hello()
.
class Car {
constructor(company, model) {
this.company = company;
this.model = model;
}
hello() {
console.log(`Hello from Car: ${this.model} by ${this.company}`);
}
}
class CarYear extends Car {
constructor(company, model, year) {
super(company, model);
this.year = year;
}
hello() {
// Call parent's version first
super.hello();
// Add extra behavior
console.log(`This model was launched in ${this.year}.`);
}
}
const myCar = new CarYear("Toyota", "Fortuner", 2022);
myCar.hello();
// Output:
// Hello from Car: Fortuner by Toyota
// This model was launched in 2022.
Static Methods
In JavaScript, static methods belong to the class itself, not to the objects created from it.
That means:
- You can’t call a static method on an object (like
carObject.info()
). - You must call it on the class (like
Car.info()
).
Static methods are defined using the static
keyword within a class declaration.
class Car {
constructor(company, model) {
this.company = company;
this.model = model;
}
// Instance method — works on object
start() {
console.log(`${this.model} of ${this.company} is starting...`);
}
// Static method — works on class
static info() {
console.log("This is a Car class.");
}
}
// Create an object
const carObject = new Car("Ford", "Ecosport");
// Call instance method
carObject.start(); // Ecosport of Ford is starting...
// Call static method
Car.info(); // This is a Car class.
// carObject.info(); // Error: carObject.info is not a function
When to Use Static vs Instance Methods
- Instance methods (like
start()
) work on individual objects and can access instance properties viathis
. - Static methods (like
info()
) belong to the class itself and cannot access instance properties. Use them for utility functions or actions that don’t depend on a specific object.
For example: Car.info()
can show general info about the class, while carObject.start()
controls a specific car.
Getters and Setters
In JavaScript classes, getters and setters let you control how properties are accessed and updated.
get
lets you access a propertyset
lets you update a property with custom logic or validation
For example:
class Car {
constructor(company, model) {
this._company = company; // convention: underscore means "private"
this._model = model;
}
// Getter for the company property
get company() {
return this._company;
}
// Setter for the company property
set company(newCompany) {
if (typeof newCompany === "string") {
this._company = newCompany;
} else {
console.log("company must be a string");
}
}
// Getter for the model property
get model() {
return this._model;
}
// Setter for the model property
set model(newModel) {
if (typeof newModel === "string") {
this._model = newModel;
} else {
console.log("model must be a string");
}
}
}
// Create an object
const carObject = new Car("Ford", "Ecosport");
// Access properties like normal variables
console.log(carObject.company); // Ford
console.log(carObject.model); // Ecosport
// Update using setters
carObject.company = "Toyota";
carObject.model = "Fortuner";
console.log(carObject.company); // Toyota
console.log(carObject.model); // Fortuner
// Try setting invalid data
carObject.model = 123; // model must be a string
Here,
- The
Car
class has getter and setter methods for thecompany
andmodel
properties. - The getters allow you to retrieve the values of these properties, while the setters provide a way to validate and set new values for the properties.
- The underscores
_company
and_model
are used to indicate that these properties are meant to be private to theCar
class and should not be accessed or modified directly from outside the class.
Private Fields
In modern JavaScript, you can create truly private properties in a class using the #
prefix. These private fields can only be accessed within the class and are not accessible outside.
Example:
class Car {
#engineNumber; // private field
constructor(company, model, engineNumber) {
this.company = company;
this.model = model;
this.#engineNumber = engineNumber; // initialize private field
}
getEngineNumber() {
return this.#engineNumber;
}
}
const myCar = new Car("Tesla", "Model S", "ENG12345");
console.log(myCar.company); // Tesla
console.log(myCar.getEngineNumber()); // ENG12345
console.log(myCar.#engineNumber); // SyntaxError: Private field '#engineNumber' must be declared in an enclosing class
Private fields help with encapsulation by hiding sensitive data inside the class.
instanceof Operator
The instanceof
operator is used to check if an object was created from a specific class (or constructor function).
For example:
// Parent class
class Car {
constructor(company, model) {
this.company = company;
this.model = model;
}
}
// Child class
class CarYear extends Car {
constructor(company, model, year) {
super(company, model);
this.year = year;
}
}
// Creating object using the Car constructor
const carObject = new Car("Ford", "Ecosport");
// Creating object using the CarYear constructor
const CarYearObject = new CarYear("Ford", "Ecosport", 2013);
// Using instanceof operator
console.log(carObject instanceof Car); // Output: true
console.log(carObject instanceof CarYear); // Output: false
console.log(CarYearObject instanceof Car); // Output: true
console.log(CarYearObject instanceof CarYear); // Output: true
Here,
- You have a
Car
class and aCarYear
class that inherits fromCar
. - You create
instances(objects)
of both classes and then use theinstanceof
operator. carObject
is an instance ofCar
, but it's not an instance ofCarYearObject
.CarYearObject
is an instance of bothCar
andCarYear
.