AvnishYadav
WorkProjectsBlogsNewsletterSupportAbout
Work With Me

Avnish Yadav

Engineer. Automate. Build. Scale.

© 2026 Avnish Yadav. All rights reserved.

The Automation Update

AI agents, automation, and micro-SaaS. Weekly.

Explore

  • Home
  • Projects
  • Blogs
  • Newsletter Archive
  • About
  • Contact
  • Support

Legal

  • Privacy Policy

Connect

LinkedInGitHubInstagramYouTube
From Prototypes to ES6 Classes: Understanding JavaScript's Object-Oriented Nature
2026-02-21

From Prototypes to ES6 Classes: Understanding JavaScript's Object-Oriented Nature

7 min readEngineeringConceptsJavaScriptWeb DevelopmentOOPES6Computer Science

A technical breakdown of JavaScript's inheritance model. We tear down the 'class' keyword to reveal the prototype chain underneath, comparing constructor functions with ES6 syntax to understand how objects actually delegate behavior.

If you come from a background in Java, C#, or C++, JavaScript’s object-oriented model can feel like a hallucination. You see the class keyword, you see new, and you see extends. You assume you are working with classical inheritance: blueprints creating copies.

You aren't.

JavaScript is one of the few mainstream languages that uses prototypal inheritance. Despite the modernize syntax introduced in ES6 (ECMAScript 2015), the underlying engine hasn't changed. The class keyword is largely syntactic sugar—a cosmetic layer designed to make the language feel more familiar to developers coming from class-based languages.

To build complex systems, debug effectively, or optimize performance in JavaScript, you need to understand what is happening under the hood. Let's dismantle the syntax and look at the machinery.

The Mental Model: Delegation vs. Blueprints

In classical OOP (like Java), a Class is a blueprint. When you instantiate an object, you are copying the structure from the blueprint into memory. The instance has its own copy of the description.

In JavaScript, we don't copy. We link.

When you create an object in JavaScript, it keeps a hidden link to another object (its prototype). If you try to access a property or method on the object that doesn't exist, the engine doesn't throw an error immediately. It travels up that link to the prototype to see if it exists there. This is Behavior Delegation.

1. The Old School: Functional Instantiation and Prototypes

Before ES6, if we wanted to create a repeatable object structure, we used constructor functions. This is the rawest way to see how memory is handled.

// The Constructor Function
function Robot(name, type) {
    this.name = name;
    this.type = type;
    // Note: We do NOT define methods inside here.
}

// Adding a method to the Prototype
Robot.prototype.greet = function() {
    return `I am ${this.name}, a unit of type ${this.type}.`;
};

const unit1 = new Robot("RX-78", "Gundam");
const unit2 = new Robot("Eva-01", "Evangelion");

console.log(unit1.greet()); // I am RX-78, a unit of type Gundam.
console.log(unit1.greet === unit2.greet); // true

Why is this important?

If we had defined this.greet = function()... inside the Robot function, every single time we created a new robot, the engine would create a brand new copy of that function in memory. By attaching it to the prototype, the function exists in memory exactly once. Both unit1 and unit2 contain a hidden reference (__proto__) pointing to Robot.prototype.

2. The Syntactic Sugar: ES6 Classes

When ES6 arrived, it introduced the class keyword. It looks cleaner, but let's verify that it does the exact same thing.

class RobotClass {
    constructor(name, type) {
        this.name = name;
        this.type = type;
    }

    greet() {
        return `I am ${this.name}, a unit of type ${this.type}.`;
    }
}

const unit3 = new RobotClass("Wall-E", "Compactor");

console.log(typeof RobotClass); // "function" ... Wait, what?
console.log(RobotClass.prototype.greet); // [Function: greet]

Even though we declared it as a class, JavaScript still sees it as a function. The greet method was automatically added to the prototype object, just like we did manually in the previous example.

The class syntax is an abstraction layer. It handles the boilerplate of setting up the prototype chain for you, preventing common errors (like forgetting to call a constructor with new).

3. Inheritance: The Prototype Chain

The differences become stark when we deal with inheritance. Let's create an AttackRobot that inherits from Robot.

The ES6 Way (Easy)

class AttackRobot extends RobotClass {
    constructor(name, type, weapon) {
        super(name, type); // Calls the parent constructor
        this.weapon = weapon;
    }

    attack() {
        return `${this.name} attacks with ${this.weapon}!`;
    }
}

This is readable. extends links the prototypes, and super runs the parent initialization code.

The Prototype Way (What actually happens)

If you were to transpile that ES6 code via Babel to run in an older browser, here is the logic it implements explicitly:

function AttackRobotProto(name, type, weapon) {
    // 1. Call the parent constructor explicitly with 'this' context
    Robot.call(this, name, type);
    this.weapon = weapon;
}

// 2. Link the prototypes
// We create a new object that delegates to Robot.prototype
AttackRobotProto.prototype = Object.create(Robot.prototype);

// 3. Fix the constructor reference (otherwise it points to Robot)
AttackRobotProto.prototype.constructor = AttackRobotProto;

// 4. Add the subclass method
AttackRobotProto.prototype.attack = function() {
    return `${this.name} attacks with ${this.weapon}!`;
};

This reveals the mechanism. Object.create allows us to create an empty object that purely links to another object. This creates the chain.

4. Visualizing the Chain

When you call attackRobotInstance.greet(), the lookup process is:

  1. Check Instance: Does the object attackRobotInstance have a method named greet? No.
  2. Check Prototype: Does AttackRobotProto.prototype have greet? No (it only has attack).
  3. Check Parent Prototype: Does Robot.prototype have greet? Yes. Execute it.

If it went all the way to Object.prototype and didn't find it, it would return undefined.

5. Why This Matters for Developers

You might ask, "If classes work, why care about the prototypes?"

1. Patching and Polyfills

Because JavaScript objects are dynamic, you can add functionality to built-in objects. This is how polyfills work. If a browser doesn't support Array.prototype.includes, you can add it to the prototype, and suddenly every array in your application has that method.

2. 'this' Context

The biggest pain point in React or Node.js development usually revolves around the this keyword. In a class-based language, this is always the instance. In JavaScript, this is determined by how the function is called, not where it is defined. Understanding that methods are just functions attached to a prototype object helps explain why you lose context when passing a method as a callback.

3. Memory Optimization

If you are building a system involving thousands of entities (like a game or a data visualization dashboard), understanding the difference between instance properties (memory per object) and prototype methods (shared memory) is critical for performance.

Conclusion

ES6 classes are a fantastic addition to the language. They clean up our code, enforce strict mode, and provide a standardized syntax for inheritance. However, they are a facade.

As an engineer, you shouldn't view JavaScript objects as static blueprints. View them as dynamic collections of properties with a delegation link to other objects. Once you grasp the chain, you grasp the language.

Share

Comments

Loading comments...

Add a comment

By posting a comment, you’ll be subscribed to the newsletter. You can unsubscribe anytime.

0/2000