
From Prototypes to ES6 Classes: Understanding JavaScript's Object-Oriented Nature
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:
- Check Instance: Does the object
attackRobotInstancehave a method namedgreet? No. - Check Prototype: Does
AttackRobotProto.prototypehavegreet? No (it only hasattack). - Check Parent Prototype: Does
Robot.prototypehavegreet? 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.
Comments
Loading comments...