DEV Community

Cover image for In-depth details of Class in JavaScript
capscode
capscode

Posted on • Edited on • Originally published at capscode.in

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 
Enter fullscreen mode Exit fullscreen mode
  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

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 parent 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

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) 
Enter fullscreen mode Exit fullscreen mode
  • Each time you create an instance of Person, a new copy of greet() 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) 
Enter fullscreen mode Exit fullscreen mode
  • greet() is only stored once in Person.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) 
Enter fullscreen mode Exit fullscreen mode

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) 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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" 
Enter fullscreen mode Exit fullscreen mode

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... 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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?

  • Hide implementation details.
  • Prevent accidental external access.

Summary:

Access Pattern Private Method Accessible?
Inside class Yes
From instance No
From subclass No

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 
Enter fullscreen mode Exit fullscreen mode

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! 
Enter fullscreen mode Exit fullscreen mode

Why Use Getters & Setters?

  • Protect properties from invalid values.
  • Format or modify values dynamically (₹ 1200 instead of 1200)

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 
Enter fullscreen mode Exit fullscreen mode

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. 
Enter fullscreen mode Exit fullscreen mode

Why Use Inheritance?

  • Avoids code duplication.
  • Makes the code more organized and reusable.

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)
  2. Truly private fields declared with # are not accessible or inherited by subclasses
  3. They are scoped only to the class they are defined in
  4. If you define a private method with the same name in the child class, it’s not overriding — it’s a completely separate method.

Summary:

Feature Inherited? Notes
Public Methods/Fields Yes Via Parent.prototype.
Static Methods/Fields Yes Accessed via Child.staticMethod().
Protected (_field) Yes* Convention only (no JS enforcement).
Private (#field) No Only accessible within the parent class.
Constructor Logic No Must 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:

  • JavaScript uses prototype-based inheritance under the hood. When you call a method on an instance:
  • JavaScript looks for the method on the instance.
  • If not found, it looks up the prototype chain.
  • If the child class has a method with the same name, it will override the parent’s method at that point in the chain.

Important points to note

  • Private methods (#methodName) cannot be overridden because they're not part of the prototype chain.
  • Static methods can also be overridden if redefined in the child class using the static keyword.
  • Private static methods (static #methodName() {}) can’t be overridden.
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. 
Enter fullscreen mode Exit fullscreen mode

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. 
Enter fullscreen mode Exit fullscreen mode

✅ 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"); } } 
Enter fullscreen mode Exit fullscreen mode

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) 
Enter fullscreen mode Exit fullscreen mode

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. 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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

NOTE:

  • Animal's #speak() can only be called inside Animal.
  • Dog's #speak() can only be called inside Dog.
  • There's no overriding happening because private members are not part of the prototype chain.
  • Subclasses cannot override private methods.
  • To enable overriding, use public or conventionally protected methods.

Summary

Feature Can 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) 
Enter fullscreen mode Exit fullscreen mode

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." 
Enter fullscreen mode Exit fullscreen mode

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." 
Enter fullscreen mode Exit fullscreen mode

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

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)