×

In-depth details of Class in JavaScript

Introduction to Classes

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.

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
  1. Cleaner & More Readable compared to functions with prototypes.
  2. Encapsulation: Methods and properties are inside the class.
  3. Inheritance: Easily extend functionality using extends.

What is constructor()?

constructor() is a special method for creating and initializing objects.

Advantages of Classes Over Constructor function & Plain Objects

Read more about Constructor function here

FeatureObject LiteralsConstructor FunctionsClasses (ES6)
Code ReusabilityNo reusabilityReusable with newBest for reuse
EncapsulationHard to group methods/dataUses prototype for methodsMethods inside class
InheritanceNot possiblePossible but complexEasy with extends
ReadabilitySimple for small casesVerbose with prototypesClean and structured

When to Use Classes?

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

In the above example, greet() method is automatically stored in Person.prototype

Key Takeaway:

  1. In Constructor Functions, we manually define methods on prototype to avoid duplication.
  2. 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)

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)

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)

Key Takeaways

  1. Class methods (like greet()) are memory efficient because they are stored once in the prototype and shared across all instances.
  2. Memory is not wasted, even if you create 1000 objects, the greet() function exists only once in memory.
  3. Method execution (calling greet()) happens only when needed, but the function itself is already available in the prototype.
  4. Same memory efficiency as manually using prototype, but with cleaner syntax.

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. Even though ES6 class methods look like instance methods, they are actually prototype methods by default.


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.


Public, Private, and Protected Fields

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 car = new Car("Tesla"); console.log(car.brand); // Tesla (Accessible) car.start(); // Tesla is starting...

Private Fields (#)

Fields (properties) can be made truly private in JavaScript classes by prefixing them with #. Private fields cannot be accessed outside the class.

class BankAccount { #balance = 0; // Private property constructor(owner) { this.owner = owner; // Public property } 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

Private Methods (#method())

Methods can be made truly private in JavaScript classes by prefixing them with #. Private methods cannot be accessed outside the class.

class Logger { #formatMessage(message) { // Private method console.log(message); } logMessage(message) { this.#formatMessage(message); } } const logger = new Logger(); logger.logMessage("System updated."); // System updated. logger.#formatMessage("Test"); // Error

Key Points:

  1. Private fields start with # and cannot be accessed outside the class.
  2. They cannot be modified directly (account.#balance = 500 → Error).
  3. Use them when you want to hide internal data.
  4. Private fields are NOT accessible in subclasses.
  5. Private methods are only accessible inside the class they are defined in.

Why Use Private Methods?

Summary:

Access PatternPrivate Method Accessible?
Inside classYes
From instanceNo
From subclassNo

Protected Fields ( _ ) (Convention Only)

Fields (properties) can be made protected in JavaScript classes by prefixing them with _. 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:

  1. Accessible inside the class
  2. Accessible inside subclasses (inherited classes)
  3. Not meant to be accessed from outside the class (but technically still possible)

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?


Static Methods and Properties

We can define the static members using the static keyword. 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

Inheritance is a concept in OOPs where one class (called the subclass or child class) inherit properties and methods from another class (called the superclass or parent class). It promotes code reusability, extensibility, and organizational clarity.

In JavaScript, inheritance is built on top of its prototype-based system — though with ES6 classes, it provides a cleaner, more familiar syntax.

In JS we can implements inheritance using the extends keyword in class-based syntax, which sets up the prototype chain between the child class and the parent class.

The subclass inherits both properties and methods from the parent.

The super() function is used inside the subclass constructor to call the parent's constructor.

Methods can be overridden in the subclass.

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.

Why Use Inheritance?

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

  1. Private Fields (#field)

Summary:

FeatureInherited?Notes
Public Methods/FieldsYesVia Parent.prototype.
Static Methods/FieldsYesAccessed via Child.staticMethod().
Protected (_field)Yes*Convention only (no JS enforcement).
Private (#field)NoOnly accessible within the parent class.
Constructor LogicNoMust call super() to reuse parent constructor logic.

Method overriding

Method overriding is an object-oriented programming (OOP) concept where a child (subclass) class provides a specific implementation of a method that is already defined in its parent (superclass).

When the overridden method is called from an instance of the subclass, the subclass’s version is executed instead of the parent’s version.

How It Works in JavaScript:

Important points to note

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.

super Keyword

super is used to access and call functions or constructors on an object's parent class. You must call super() before accessing this in a subclass constructor.

  1. 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.

  1. 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.


Instance Method in classes?

If you define a method inside the constructor function or class 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)

Overriding instance method

Lets take an example

class Animal { constructor(name) { this.name = name; this.speak = function() { console.log(`${this.name} makes a sound.`); }; } } class Dog extends Animal { speak() { console.log(`${this.name} makes noise.`); } } const dog = new Dog("Rocky"); dog.speak(); //Rocky makes a sound.

Why does it executed the method of Animal and not of the Dog class? Methods defined inside a constructor are not inherited via the prototype chain. They are created as a new function and attached directly to each instance when that constructor runs. In JavaScript, instance properties (like this.speak from the constructor) will always take precedence over prototype properties when looking up methods via the prototype chain.

How Property Lookup Works in JavaScript (The Lookup Chain)

When you access a property or method on an object, JavaScript follows this exact sequence:

  1. Check the object (instance) itself. 👉 If the property/method is found here, it’s used immediately.

  2. If not found, check the object's prototype (__proto__ or [[Prototype]]) 👉 Moves up to the prototype object linked via Object.getPrototypeOf(obj) or obj.__proto__

  3. Keep moving up the prototype chain 👉 Until either:

    • It finds the property/method.
    • Or reaches Object.prototype, and if still not found — returns undefined.

Can we override private methods and fields ?

You cannot override private fields or methods (declared with #) in JavaScript — they are completely encapsulated within the class they are defined in.

Example:

class Animal { #speak() { return "Animal sound"; } makeSound() { console.log(this.#speak()); } } class Dog extends Animal { #speak() { return "Dog barks"; } } const dog = new Dog(); dog.makeSound(); // Output: Animal sound dog.speak() // Uncaught SyntaxError: Private field '#speak' must be declared in an enclosing class

Why dog.speak() throws error we will see in the below section (i.e private method)

NOTE:

Summary

FeatureCan Be Overridden?Inherited?Accessible in Subclass
Public Method/Field✅ Yes✅ Yes✅ Yes
_Protected Convention✅ Yes✅ Yes✅ Yes
#Private Field/Method❌ No❌ No❌ No

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.

How we can say static member are overridden ?

class Animal { static info() { console.log("This is the Animal class."); } } class Dog extends Animal { static info() { console.log("This is the Dog class."); } } // Now, let's call them: Animal.info(); // 👉 "This is the Animal class." Dog.info(); // 👉 "This is the Dog class."

The static method is considered "overridden" because calling Dog.info() does NOT use Animal.info() — it uses its own definition.

But if you don’t override it in Dog, then:

class Dog extends Animal { // no info() method here } Animal.info(); // 👉 "This is the Animal class." Dog.info(); // 👉 "This is the Animal class."

✅ Now, Dog inherits Animal’s static method because it didn’t define its own.

Why Use Static Methods?

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.