In-depth details of Class in JavaScript
A class in JavaScript is a blueprint for creating objects. It allows you to define reusable object structures with properties and methods, making object creation and management more structured and efficient.
JavaScript classes are a blueprint for creating objects. They introduce object-oriented programming (OOP) concepts like encapsulation, inheritance, and abstraction.
Before ES6, JavaScript used constructor functions and prototypes to create and manage objects. With ES6, class syntax was introduced to provide a cleaner, more intuitive way to define object templates.
Why Do We Need Classes If We Already Have Objects?
JavaScript allows creating objects without classes using object literals, but classes provide advantages in scalability, maintainability, and reusability. Let's explore:
class Person { constructor(name, age) { this.name = name; this.age = age; } greet() { console.log(`Hello, my name is ${this.name}`); } } const person = new Person("Charlie", 35); person.greet(); // Hello, my name is Charlie
- Cleaner & More Readable compared to functions with prototypes.
- Encapsulation: Methods and properties are inside the class.
- Inheritance: Easily extend functionality using extends.
Advantages of Classes Over Constructor function & Plain Objects
Read more about Constructor function here
Feature | Object Literals | Constructor Functions | Classes (ES6) |
---|---|---|---|
Code Reusability | No reusability | Reusable with new | Best for reuse |
Encapsulation | Hard to group methods/data | Uses prototype for methods | Methods inside class |
Inheritance | Not possible | Possible but complex | Easy with extends |
Readability | Simple for small cases | Verbose with prototypes | Clean and structured |
When to Use Classes?
- When you need to create multiple objects with the same structure.
- When your objects have methods and behavior (not just data).
- When you need inheritance (extend features from a base class).
- When writing large-scale applications for better maintainability.
NOTE: If you're just defining a single, simple object, object literals are fine. But for structured, scalable code, classes are the best approach.
Why Don't We Need prototype in ES6 Classes?
With ES6 classes, methods are automatically added to the prototype, so we don't need to manually define them.
Must read about __proto__
, [[Prototype]]
, .prototype
Key Takeaway:
- In Constructor Functions, we manually define methods on prototype to avoid duplication.
- In ES6 Classes, methods are automatically added to prototype, making code cleaner.
Does the greet()
Method in a Class Consume Memory Before Calling It?
Yes, but in an efficient way. In JavaScript classes, methods like greet()
are stored in the prototype of the class only once and are shared among all instances.
Even though greet()
is not executed until you call it, it still exists in memory as part of the class's prototype. However, since it's only stored once (not duplicated in each object), it is memory efficient compared to defining it inside the constructor.
Comparing Memory Efficiency: Class vs. Constructor Function
1. Constructor Function Without Prototype (Memory Waste)
function Person(name) { this.name = name; this.greet = function() { // Each instance has its own copy of greet() console.log(`Hello, my name is ${this.name}`); }; } const person1 = new Person("Alice"); const person2 = new Person("Bob"); console.log(person1.greet === person2.greet); // false (Different function instances)
- Each time you create a
Person
, a new copy ofgreet()
is created in memory. - If you create 1000 objects, there are 1000 separate
greet()
functions, which wastes memory.
2. Constructor Function With Prototype (Efficient)
Read more about Constructor function here
function Person(name) { this.name = name; } Person.prototype.greet = function() { // Stored once in prototype console.log(`Hello, my name is ${this.name}`); }; const person1 = new Person("Alice"); const person2 = new Person("Bob"); console.log(person1.greet === person2.greet); // true (Same function reference)
greet()
is only stored once inPerson.prototype
and shared by all instances.- More memory-efficient than defining
greet
inside the constructor.
Read more about __proto__
, [[Prototype]]
, .prototype
3. ES6 Class (Automatically Optimized)
class Person { constructor(name) { this.name = name; } greet() { // Automatically added to prototype console.log(`Hello, my name is ${this.name}`); } } const person1 = new Person("Alice"); const person2 = new Person("Bob"); console.log(person1.greet === person2.greet); // true (Same function reference)
- In classes, methods like
greet()
are automatically stored in the prototype, so we don't need to manually add them. - Same memory efficiency as manually using
prototype
, but with cleaner syntax.
Key Takeaways
- Class methods (like
greet()
) are memory efficient because they are stored once in the prototype and shared across all instances. - Memory is not wasted, even if you create 1000 objects—the
greet()
function exists only once in memory. - Method execution (calling
greet()
) happens only when needed, but the function itself is already available in the prototype.
So yes, class methods are memory efficient compared to defining methods inside the constructor.
NOTE: In ES6 classes, methods are automatically added to the prototype of the class.
🔹 How Prototype Methods Work in Classes
In ES6 classes, all methods inside the class body are added to ClassName.prototype
.
Example:
class Person { constructor(name) { this.name = name; } // This is a prototype method greet() { return `Hello, my name is ${this.name}`; } } const alice = new Person("Alice"); const bob = new Person("Bob"); console.log(alice.greet()); // "Hello, my name is Alice" console.log(bob.greet()); // "Hello, my name is Bob" // Checking the prototype console.log(alice.__proto__ === Person.prototype); // true console.log(alice.greet === bob.greet); // true (Same function from prototype)
✔ All instances share the same greet()
method because it is stored in Person.prototype
.
🔹 Where Is the greet()
Method Stored?
Even though it looks like greet()
is inside each instance, it's actually in Person.prototype
:
console.log(Person.prototype); // { constructor: ƒ Person(), greet: ƒ greet() } console.log(alice.hasOwnProperty("greet")); // false (greet is not in alice itself, it's in the prototype) console.log(Object.getPrototypeOf(alice) === Person.prototype); // true
This is the same behavior as manually assigning methods to Person.prototype
in constructor functions.
Can We Add Methods to Person.prototype
Manually?
Yes! Even after defining a class, you can manually add prototype methods.
Person.prototype.sayBye = function () { return `Goodbye from ${this.name}`; }; console.log(alice.sayBye()); // "Goodbye from Alice" console.log(bob.sayBye()); // "Goodbye from Bob"
This works because ES6 classes still use prototypes under the hood.
Even though ES6 class methods look like instance methods, they are actually prototype methods by default.
When Is a Method an "Instance Method" in classes?
If you define a method inside the constructor, then it becomes an instance method (separate for each object):
class Person { constructor(name) { this.name = name; this.greet = function() { // Instance method (not on prototype) return `Hello, ${this.name}`; }; } } const alice = new Person("Alice"); const bob = new Person("Bob"); console.log(alice.greet === bob.greet); // false (Different function instances) console.log(alice.hasOwnProperty("greet")); // true (Stored on each instance)
Inheritance
class Animal { constructor(name) { this.name = name; } speak() { console.log(`${this.name} makes a noise.`); } } class Dog extends Animal { // Inherits everything from Animal bark() { console.log(`${this.name} barks.`); } } const dog = new Dog("Scooby"); dog.speak(); // Scooby makes a noise.
Method overriding
class Animal { constructor(name) { this.name = name; } speak() { console.log(`${this.name} makes a noise.`); } } class Dog extends Animal { // Inherits everything from Animal speak() { console.log(`${this.name} barks.`); } } const dog = new Dog("Scooby"); dog.speak(); // Scooby barks.
NOTE: If you define a private method with the same name in the child class, it’s not overriding — it’s a completely separate method.
What is inherited by a subclass (extends)
All public properties and methods of the parent class All protected fields (by convention, prefixed with _) You can override or extend them in the child class
What is not inherited
- Private Fields (#field) Truly private fields declared with # are not accessible or inherited by subclasses They are scoped only to the class they are defined in
super Keyword
super
is used to access and call functions or constructors on an object's parent class.
- super() inside a constructor Used to call the parent class's constructor. It must be called before using this in a subclass constructor.
class Vehicle { constructor(brand) { this.brand = brand; } describe() { return `This is a ${this.brand}.`; } } class Car extends Vehicle { constructor(brand, model) { super(brand); // Calls Vehicle constructor this.model = model; } describe() { return `${super.describe()} Model: ${this.model}.`; } } const myCar = new Car("Tesla", "Model S"); console.log(myCar.describe()); // This is a Tesla. Model: Model S.
✅ Without super(brand), the subclass can't initialize this.name. ❗ If you skip super() in a subclass constructor, you’ll get Error.
- super.method() inside a method Used to call a method from the parent class.
class Animal { speak() { console.log("Animal speaks"); } } class Dog extends Animal { speak() { super.speak(); // calls Animal's speak() console.log("Dog barks"); } }
Useful when you want to want to add extra logic in child method.
NOTE: JavaScript does not support multiple inheritance directly through the extends keyword. However, multiple inheritance can be simulated using mixins or composition.
2. Public, Private, and Protected Fields
2.1 Public Fields (Default)
All properties and methods are public by default.
class Car { constructor(brand) { this.brand = brand; // Public property } start() {// Public method console.log(`${this.brand} is starting...`); } } const car1 = new Car("Tesla"); console.log(car1.brand); // Tesla (Accessible) car1.start(); // Tesla is starting...
2.2 Private Fields (#)
Private fields cannot be accessed outside the class.
class BankAccount { #balance = 0; // Private property constructor(owner) { this.owner = owner; } deposit(amount) { this.#balance += amount; console.log(`Deposited: $${amount}`); } getBalance() { return this.#balance; } } const account = new BankAccount("Alice"); account.deposit(100); console.log(account.getBalance()); // 100 console.log(account.#balance); // Error: Private field
2.2.1. Private Methods (#method())
Private methods cannot be accessed outside the class.
class Logger { logMessage(message) { this.#formatMessage(message); } #formatMessage(message) { // Private method console.log(message); } } const logger = new Logger(); logger.logMessage("System updated."); // System updated. logger.#formatMessage("Test"); // Error
Key Points:
- Private fields start with # and cannot be accessed outside the class.
- They cannot be modified directly (account.#balance = 500 → Error).
- Use them when you want to hide internal data.
- Private fields are NOT accessible in subclasses.
Why Use Private Methods?
- Hide implementation details.
- Prevent accidental external access.
2.3 Protected Fields (_) (Convention Only)
JavaScript doesn't have true protected fields, but _ is a naming convention to indicate internal use.
class Employee { constructor(name, salary) { this.name = name; this._salary = salary; // Convention: Internal use } getSalary() { return this._salary; } } const emp = new Employee("Bob", 5000); console.log(emp._salary); // Works, but should be avoided
NOTE: _salary
is not truly private, just a hint that it's for internal use.
A protected field is:
- Accessible inside the class
- Accessible inside subclasses (inherited classes)
- Not meant to be accessed from outside the class (but technically still possible)
3. Getters & Setters
Used to control access to properties while keeping them private. getters and setters can be used for both private and public fields. However, they are most useful for encapsulating private fields to prevent direct access and modification.
class Product { #price; constructor(name, price) { this.name = name; this.#price = price; } get price() { return `₹ ${this.#price}`; } set price(newPrice) { if (newPrice < 0) { console.log("Price cannot be negative!"); } else { this.#price = newPrice; } } } const item = new Product("Laptop", 1200); console.log(item.price); // ₹ 1200 (Getter) item.price = -500; // Price cannot be negative!
Why Use Getters & Setters?
- Protect properties from invalid values.
- Format or modify values dynamically (₹ 1200 instead of 1200)
4. Static Methods and Properties
Static methods & properties belong to the class itself, not instances.
class MathHelper { static PI = 3.14159; static square(num) { return num * num; } } console.log(MathHelper.PI); // 3.14159 console.log(MathHelper.square(4)); // 16 const helper = new MathHelper(); console.log(helper.PI); // Undefined console.log(helper.square(4)); // TypeError: helper.square is not a function
Inheritance and Static Members:
Static members are inherited by subclasses and can be called on them directly:
class MathHelper { static PI = 3.14159; //static property static square(num) { //static method return num * num; } } // Inheriting from MathHelper class AdvancedMathHelper extends MathHelper { static cube(num) { //static method return num * num * num; } static areaOfCircle(radius) { // Using the inherited static property PI //Inside a static method, this refers to the class itself return this.PI * this.square(radius); } } console.log(AdvancedMathHelper.square(4)); // 16 (inherited static method) console.log(AdvancedMathHelper.cube(3)); // 27 (own static method) console.log(AdvancedMathHelper.areaOfCircle(5)); // 78.53975 (uses inherited PI and square)
NOTE: Static members are inherited and can be accessed using this or the class name inside the subclass.
Why Use Static Methods?
- They don't depend on instance properties.
- Defining constant values that are related to the class but remain the same across all instances.
- Creating methods that return new instances of the class based on certain parameters or conditions.
- Since static members are shared across all instances, they can help conserve memory when the same data or behavior is needed across instances.
NOTE: Static methods do not have access to instance properties or methods. Attempting to reference this within a static method refers to the class itself, not an instance.
What if areaOfCircle is not static
class MathHelper { static PI = 3.14159; static square(num) { return num * num; } } class AdvancedMathHelper extends MathHelper { areaOfCircle(radius) { // 'this.constructor' refers to the class (AdvancedMathHelper) return this.constructor.PI * this.constructor.square(radius); } } const helper = new AdvancedMathHelper(); console.log(helper.areaOfCircle(5)); // Output: 78.53975
In an Instance Method: this refers to the instance. this.constructor refers to the class.
In a Static Method:
this already refers to the class itself.
So this.constructor refers to the constructor of the class, which is usually Function, not useful here.
Hence the below will not work in static areaOfCircle()
method.
// Wrong — this.constructor is not what you want here return this.constructor.PI * this.constructor.square(radius); // Doesn't work
Summary
JavaScript classes provide a structured and efficient way to create objects using a blueprint pattern. Introduced in ES6, they offer a cleaner syntax compared to traditional constructor functions while supporting core object-oriented programming principles like encapsulation, inheritance, and abstraction. Classes contain constructors for initializing objects, methods that are automatically assigned to the prototype for memory efficiency, and support inheritance through the extends
keyword with super()
for accessing parent class members. They also include static properties/methods for class-level operations and multiple access modifiers - public by default, truly private fields using #
prefix for encapsulation, and protected fields (conventionally marked with _
) for internal use. Additional features like getters/setters allow controlled property access, while private methods enable implementation hiding. Under the hood, classes still use JavaScript's prototypal inheritance, but provide a more intuitive and maintainable syntax for object creation and organization, especially beneficial for large-scale applications requiring reusable components with shared behavior. The memory-efficient prototype system ensures method sharing across instances, and the class syntax makes inheritance hierarchies clearer than manual prototype manipulation.