Mastering the Blueprint: A Deep Dive into JavaScript’s Object-Oriented Heart

In the early days of the web, JavaScript was a lightweight scripting tool. But as our applications grew into complex ecosystems, we needed a way to organize our logic. Enter Object-Oriented Programming (OOP). While JavaScript is famously "prototypal," we now have modern syntax that makes it look and feel like classical languages.
Today, we’re going to pull back the curtain on how objects work, why "Classes" are actually a clever lie, and how we can master the four pillars of OOP.
1. The Myth of the "Class": It’s Just Syntactic Sugar
In languages like Java, a Class is a blueprint and an Object is the house. In JavaScript, we only really have houses. When we write class Cricketer, we aren't actually creating a new structural type in the engine; we are using Syntactic Sugar.
Before 2015 (ES6), we had to use Constructor Functions. Under the hood, your Cricketer class is still just a function.
// What we write now:
class Cricketer {
constructor(name, role) {
this.name = name;
this.role = role;
}
}
// What JavaScript actually sees:
function Cricketer(name, role) {
this.name = name;
this.role = role;
}
We needed classes because constructor functions were messy to read and inheritance was a nightmare to chain manually. The class keyword simply gives us a cleaner, more standardized way to write what we were already doing.
2. The Prototype Powerhouse
Every object in JavaScript has a secret link to another object called its Prototype. When we try to access a property that doesn't exist on an object, JavaScript looks at the prototype.
In your code, we see a crucial distinction regarding where methods live. Look at these two ways of defining behavior:
class Debu {
constructor(name) {
this.name = name;
// This creates a NEW function for every single instance!
this.walkout = ( ) => `${this.name} is running`;
}
// This lives on the PROTOTYPE (shared by all instances)
greet() {
console.log("Hello!");
}
}
const d1 = new Debu("Jay");
const d2 = new Debu("Viru");
console.log(d1.walkout === d2.walkout); // false (Memory wasted)
console.log(d1.greet === d2.greet); // true (Efficient)
When we attach a function inside the constructor (like walkout), every time we call new Debu(), we occupy more memory. However, if we define it outside the constructor or manually via Debu.prototype.newFunc, all instances share a single reference. This is how we keep our apps performant.
Constructor
The constructor method is a special method for creating and initializing an object created with a class. There can only be one special method with the name "constructor" in a class a SyntaxError is thrown if the class contains more than one occurrence of a constructor method.
A constructor can use the super keyword to call the constructor of the super class.
class Rectangle {
constructor(height, width) { // invokes on "new"
this.height = height;
this.width = width;
}
}
Static methods and fields
The static keyword defines a static method or field for a class. Static properties (fields and methods) are defined on the class only once , instead of multiple times for each instance.
static can be used with fieldds if we want to keep them constant for every instance and also for methods if we want to access them using className direvtly indstead of any instance or object.
e.g. static methods : accessed using ClassName.staticMethodname();
e.g. class methods : accessed using this.classMethodName();
Declaring Fields in Class (Class Fields - Variables)
Class fields are similar to object properties , not variables, so we don't use keywords such as const, var, let to declare them. In JavaScript , use a special identifier syntax (#FieldName), so modifier keywords like public and private should not be used either.
class Rectangle {
height = 0; // public vars
#width; // private vars
constructor(height, width) {
this.height = height;
this.#width = width; // accessing also with #varName
}
}
3. The Four Pillars of OOP
To truly master OOP, we must lean on these four concepts:
I. Encapsulation
We bundle data and the methods that operate on that data into one unit. It’s about hiding the "guts" of our object. In modern JS, we use # to create truly private fields that can't be touched from the outside.
II. Abstraction
We hide complex implementation details and only show the essentials. When we call cricketer.introduce(), we don't care how the string is built; we just want the result.
II. Inheritance
The extends keyword is used in class declarations to create a class as a child of another constructor (either a class or a function).
If there is a constructor present in the subclass, it needs to first call super() before using this. The super keyword can also be used to call corresponding methods of super class.
class Cat {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} speaks.`);
}
}
class Lion extends Cat {
speak() {
super.speak();
console.log(`${this.name} roars .`);
}
}
const l = new Lion("Simba");
l.speak(); // Simba makes a noise. // Simbaroars.
IV. Polymorphism
The word means "many shapes." It allows us to use the same method name but have it behave differently depending on the object. A Bowler and a Batter might both have a train() method, but they do very different things.
4. The "This" Context and Closures
One of the trickiest parts of our journey is the this keyword. In your Debu class, you used an arrow function for walkout.
Why does this matter? Normal functions define this based on where they are called. Arrow functions capture this from where they are defined.
This leads us to Closures. A closure is when a function remembers its lexical scope even when it is executed outside that scope. When we do const xtra = d.walkout; xtra();, the function "remembers" the this (the object d) because of the closure created by the arrow function.
5. Why do we need all this?
We use OOP because it mirrors the real world. In our cricket example, it’s much easier to manage a Cricketer object than it is to manage 50 independent variables for names, roles, and scores. It gives our code state and behavior.
6. NOTE :
There is a common misconception that "if it's in the class, it's efficient."
While both Static and Prototype (Normal) methods are more memory-efficient than methods defined inside a constructor, they serve completely different purposes and live in different "rooms" of the memory house.
1. The Memory Location: Where do they live?
In JavaScript, everything is an object. A class is essentially a function object. When we define methods, the engine stores them in two distinct places:
Prototype Methods : Normal Methods of class
These live on the Class.prototype. When we create 1,000 instances (like new Cricketer()), those 1,000 objects do not have their own copy of the method. They all share a single reference to the one function sitting on the prototype.
Memory cost: 1 function (shared by all instances).
Access:
this.methodName()
Static Methods
These live directly on the Constructor Function itself, not on the instances. They are essentially "Utility" functions attached to the class name.
Memory cost: 1 function (shared by the class itself).
Access:
ClassName.methodName()
2. The Comparison Table
Feature | Prototype Method (Normal) | Static Method |
Storage |
|
|
Instance Access | Yes ( | No ( |
Class Access | No | Yes ( |
Purpose | Actions related to a specific player. | Actions related to the concept of Cricket. |
Memory Efficiency | High (Shared) | High (Shared) |
3. Do they have same memory consumption, if they exist only once ?
We are correct, that both save memory compared to Instance Methods (methods defined inside the constructor).
If we define a method inside the constructor using this.walkout = ..., we are creating a new function object every single time we use new. If we have 10,000 players, we have 10,000 functions in memory.
However, between Static and Prototype:
Static methods are slightly "cheaper" only because they don't require an instance to exist at all. We can call
Cricketer.getRules()without ever runningnew Cricketer().Prototype methods require us to instantiate an object to use them, which consumes memory for the object's properties (
name,role, etc.), even if the method itself is shared.
4. When to use which?
As developers, we shouldn't choose based on memory alone (since both are efficient), but based on Intent:
Using the Normal Methods (Prototype or member functions)
When the function needs to access data specific to an instance (using this).
class Cricketer {
constructor(name) { this.name = name; }
// Needs 'this.name', so it MUST be a normal method
#shoutName() { // private
console.log(this.name);
}
}
Use Static Methods
When the function is a "helper" that doesn't care who the specific player is.
class Cricketer {
// Doesn't care about 'this.name', just calculates a value
static calculateRunRate(runs, overs) {
return runs / overs;
}
}
The "Hidden" Cost
One thing we must remember:
Static methods are not inherited by instances.
If we try to call a static method from our
jayobject, it will beundefined.
Key Takeaways from our code:
typeof Cricketer: It returns
"function". Never forget: Classes are functions ( Syntactic Sugar )!undefined properties: In your code,
this.undefined = nullshows that we can name properties whatever we want, but it’s best to avoid using reserved keywords to keep our sanity.Memory Management: Always prefer prototype methods over constructor-defined functions unless you specifically need the closure of an arrow function.