JavaScript OOP Complete Guide (2026): Prototypes, Classes & SOLID
Master JavaScript OOP: prototype chain, classes, 'this' binding rules, tricky output puzzles, scratch polyfills, and production SOLID architecture.
30-Second Key Takeaways
TL;DRQuick digest before you dive deep

Mastering Object-Oriented Programming in JavaScript
If you have ever prepared for senior or staff-level JavaScript interviews at companies like Google, Meta, Amazon, or high-growth tech startups, you already know one uncomfortable truth: Object-Oriented JavaScript is the number one topic where candidates stumble.
While Java and C++ operate on rigid, compile-time class blueprints, JavaScript was designed around a dynamic prototypal delegation model. When ES6 introduced the class keyword in 2015, many developers assumed JavaScript had transformed into a classical OOP language. In reality, ES6 classes are syntactic sugar layered directly over prototype chains and lexical environments.
Interviewers exploit this exact knowledge gap. They will ask you to predict tricky this outputs, explain the difference between prototype and __proto__, hand-code polyfills for new and bind, and architect production-grade Node.js backends using SOLID principles.
Under-The-Hood Mechanics
Step-by-step ECMAScript specification rules for property lookup, prototype chain resolution, and constructor invocation.
Interactive Predictions & Polyfills
Hand-implement new, bind, instanceof, and interactive output puzzles with live explanations.
Production Node.js & SOLID
Production patterns for error hierarchies, custom EventEmitters, dependency injection, and Repository/Service architecture.
If you are preparing for an immediate interview, dive straight into Section 2 (Prototypes), Section 6 (`this` Keyword), and test yourself on Section 7 (Output Predictions). If you are designing scalable backend services, study Section 8 (Implementations), Section 9 (SOLID), and Section 10 (Node.js Backend).
1. OOP Fundamentals: JavaScript vs Classical Languages
Core paradigm shift, object instantiation patterns, the exact 4-step `new` algorithm, and defensive constructors.
What is OOP? Prototype-Based vs Class-Based
Object-Oriented Programming (OOP) is a software design paradigm organized around data structures (objects) containing properties and behaviors (methods), rather than isolated functions and imperative procedures.
In classical OOP languages like Java or C++, a Class is an immutable, compile-time blueprint stored in memory. Objects are direct instances cast from that blueprint. A class cannot modify its structure dynamically at runtime for all living instances.
In contrast, JavaScript is prototype-based. There are no static, binary-compiled classes in the V8 engine. Instead, objects link directly to other objects via hidden reference pointers ([[Prototype]]). If an object cannot find a requested property, it delegates the search up the chain.
- •Classes are blueprints: Distinct runtime entity from instances.
- •Static inheritance: Class hierarchy defined at compile time.
- •Copy-on-instantiation: Methods and fields are copied or indexed in a vtable.
- •Objects inherit from objects: No separate class entity exists in engine memory.
- •Dynamic live delegation: If prototype changes at runtime, all instances reflect it immediately.
- •No vtable duplication: Delegation traverses the prototype link on cache miss.
The 6 Ways to Create Objects in JavaScript
Interviewers love asking candidates to enumerate every way to instantiate an object. Here is the complete spectrum with practical trade-offs:
| Pattern | Prototype Link | Memory Cost | Primary Use Case |
|---|---|---|---|
| Object Literal ({}) | `Object.prototype` | Low (Fastest) | Singletons, DTOs, configs |
| Constructor Function | `Constructor.prototype` | Low (if methods on proto) | Legacy codebases, libraries |
| Object.create(proto) | Specified `proto` | Lowest | Pure delegation, `Object.create(null)` maps |
| ES6 Class | `Class.prototype` | Low (prototype shared) | Modern web apps, enterprise design |
| Factory Function | `Object.prototype` | Higher (method per instance) | True private state without `#`, closures |
What Exactly Does the `new` Keyword Do? (Step-by-Step)
This is an absolute staple at top-tier companies. When an engine executes new Constructor(...args), four distinct phases happen sequentially:
ECMAScript [[Construct]] Internal Method Lifecycle
Create Object
A new empty memory record is allocated: {}
Link Prototype
Internal [[Prototype]] is set to Constructor.prototype
Bind & Execute
Constructor called with this pointing to the new object
Return Resolution
If constructor returned an object, return that; else return the created instance
Here is how to reproduce this exact behavior in vanilla JavaScript:
return 42; or return "string";), JavaScript ignores it and returns the newly instantiated object. But if the constructor explicitly returns a reference object (like return { x: 1 }; or an array), JavaScript discards the newly created instance and returns that returned object!What Happens If You Forget `new`?
Before ES6 classes, forgetting the new keyword was the number one cause of production memory bugs and global variable poisoning.
2. Prototypes: The Holy Grail of JS Interviews
Spec definitions, chain resolution algorithm, memory allocation profiling, property shadowing, and ES2022 security.
`prototype` vs `__proto__` vs `[[Prototype]]`
Confusion between these three names is the primary reason candidates fail technical rounds. Let's establish unambiguous definitions:
Prototype Terminology Matrix
Internal Engine Slot
Hidden spec pointer inside every JavaScript object pointing to its prototype parent. Not directly accessible via standard property syntax.
Function Property
An actual property on constructor functions and classes. Becomes the [[Prototype]] of instances created via new.
Legacy Accessor
A getter/setter on Object.prototype that exposes [[Prototype]]. Deprecated in favor of Object.getPrototypeOf.
Why Put Methods on the Prototype Instead of Inside the Constructor?
Interviewers ask this to test whether you understand V8 heap allocation and garbage collection:
Methods inside Constructor
If you instantiate 10,000 objects, V8 allocates 10,000 unique closure function instances in RAM. Each closure retains its own scope chain, drastically inflating memory consumption.
10,000 instances = 10,000 function objectsMethods on Prototype
All 10,000 instances hold a lightweight reference to a single shared function object on the prototype object. Instantaneous execution with negligible memory overhead.
10,000 instances = 1 shared function objectPrototype Chain Resolution & Property Shadowing
When reading object.property, the engine performs the following lookup loop:
- Checks if
propertyis an own property on the object. - If absent, follows
[[Prototype]]to the parent object. - Recursively repeats this traversal until the property is found or it reaches
null. - If
nullis reached without finding the property, returnsundefined.
`hasOwnProperty` vs `in` vs `Object.hasOwn` (ES2022)
Senior candidates are expected to know why calling obj.hasOwnProperty() is an anti-pattern in modern production applications, and why ECMAScript introduced Object.hasOwn():
What is at the End of Every Prototype Chain? Why `Object.create(null)`?
At the top of the standard chain resides Object.prototype. And what is its prototype? null.
`instanceof` vs `typeof` vs `isPrototypeOf`
How do these three operators differ when verifying object hierarchy?
- typeof: Operates on primitive types (
"string","number","function","undefined"). Fails on objects:typeof [] === 'object'andtypeof null === 'object'(historic JS bug). - instanceof: Checks if
Constructor.prototypeexists anywhere along the object's prototype chain. Limitation: Fails across different iframes/execution contexts (realms) because each iframe has its own globalArray.prototype! - isPrototypeOf: Method on any prototype object. Directly checks whether that object exists in the target's prototype chain:
Array.prototype.isPrototypeOf([]) === true. Works even without constructors!
3. Classes (ES6+): Syntactic Sugar or True Paradigm?
Under-the-hood engine proof, TDZ hoisting semantics, hard private fields (`#`), brand checking, and static initialization blocks.
Are Classes Real or Syntactic Sugar Over Prototypes?
The canonical answer in an interview: Classes are syntactic sugar over prototypal inheritance, BUT with crucial engine-level guarantees and behavioral constraints.
| Behavior | ES5 Constructor Function | ES6 Class |
|---|---|---|
| Type | typeof F === 'function' | typeof C === 'function' |
| Call without `new` | Permitted (mutates global in non-strict) | Throws TypeError (`[[IsConstructor]]` flag) |
| Method Enumerability | Enumerable (`enumerable: true`) | Non-enumerable (`enumerable: false`) |
| Execution Mode | Sloppy mode unless explicit 'use strict' | Always strict mode inside class body |
| Hoisting | Function declaration fully hoisted | Hoisted but TDZ (Temporal Dead Zone) |
Are Classes Hoisted? (Temporal Dead Zone Explained)
Yes, classes are hoisted by the JavaScript parser, but unlike function declarations, they are initialized to an uninitialized state in the Temporal Dead Zone (TDZ). Accessing a class before its declaration line results in a ReferenceError:
Hard Private Fields (`#x`) vs `_x` Convention
For two decades, JavaScript developers used the underscore prefix (e.g. this._balance = 1000) to communicate private intent. But this was merely a soft convention; anyone could inspect or mutate _balance at runtime.
Modern ECMAScript (ES2022+) provides true hard privacy with the `#` prefix:
Object.getOwnPropertySymbols() reveals them) and closures (which cannot be shared across methods in separate instances without memory duplication), #private fields are stored in a dedicated internal slot ([[PrivateElements]]) on the object instance. They are strictly inaccessible outside the lexical text of the class body!Static Blocks (`static { ... }`) & Class Expressions
In ES2022, JavaScript introduced Static Initialization Blocks. They provide a dedicated execution scope that runs once during class evaluation, allowing you to configure static properties, perform try/catch setup, or share private fields with external helper functions:
4. Inheritance: ES6 `extends` vs ES5 Prototypal Wiring
`super()` allocation mechanics, ES5 4-step canonical pattern, extending built-ins (`Array`, `Error`), and Composition vs Mixins.
`extends` and `super`: Modern Subclassing
In ES6+, subclassing is declared using the extends keyword, and access to parent methods or constructors is handled through super:
Why MUST You Call `super()` Before Using `this`?
Almost every mid-to-senior interview will probe this question. Most candidates guess: "Because you need to copy the parent's properties first." That is incorrect.
Here is the exact ECMAScript engine truth:
this is bound when entering the function. In a derived class constructor, the environment record has an internal field [[ThisBindingStatus]] initialized to "uninitialized". Calling super() invokes the superclass constructor, returns the object instance, and sets [[ThisBindingStatus]] to "initialized". Accessing this prior to that causes the engine to throw a ReferenceError.How Inheritance Worked in ES5 (The 4-Step Classic Pattern)
Before class and extends existed, JavaScript engineers wired inheritance manually. You will frequently be asked to write this from memory on a whiteboard or coderpad:
Extending Built-ins (`Array`, `Error`, `Map`)
In ES5, subclassing built-ins like Array or Error was famously broken because internal array slots like [[DefineOwnProperty]] and magic length recalculations could not be inherited properly.
In ES6+, classes natively support extending built-ins because the base constructor initializes the object memory layout first!
Multiple Inheritance via Mixins & "Composition over Inheritance"
JavaScript does not support multiple inheritance. A class can have only one direct prototype. To share behavior across unrelated class hierarchies without deep inheritance trees, we use Factory Mixins:
The famous Gang of Four guideline states: "Favor object composition over class inheritance."
HttpError extends Error). Deep inheritance trees suffer from the Fragile Base Class problem, where modifying a parent method unexpectedly breaks distant descendants.User doesn't inherit from Mailer and Validator; it accepts or instantiates them as dependencies. Easier to unit test, mock, and refactor.5. The Four Pillars of OOP in JavaScript
Encapsulation mechanisms, simulating Abstract classes via `new.target`, Polymorphism through Duck Typing, and Overloading workarounds.
1. Encapsulation: Keeping Secrets Secret
Encapsulation is the bundling of data and the methods that operate on that data into a single unit, while restricting direct access to internal implementation details.
JavaScript offers 4 distinct ways to achieve encapsulation, each with unique trade-offs:
2. Abstraction: Simulating Abstract Classes with `new.target`
Unlike TypeScript or Java, JavaScript does not have an abstract keyword. However, you can enforce abstract classes and interfaces at runtime using the standard new.target meta-property:
3. Polymorphism: Duck Typing & Overloading Fakes
Polymorphism means "many forms" — the ability of different objects to respond to the same method call with distinct, customized behaviors.
In JavaScript, polymorphism is achieved through Method Overriding and Duck Typing. Because JavaScript does not support static signature overloading (defining multiple methods with the same name but different parameters), we fake overloading via argument checking:
4. Inheritance: Dynamic Prototypal vs Static Classical
In classical OOP, inheritance is a copy-down operation from parent class to child class. In JavaScript, inheritance is an upward delegation pointer. The child does not copy the parent's properties; it dynamically delegates property lookups to the parent object.
6. The `this` Keyword: The Engine's Binding Algorithm
The 4 binding rules in order of precedence, callback context loss, arrow class field memory profiling, and handwritten `bind`.
The Four Binding Rules (Order of Precedence)
In JavaScript, this is not authored at declaration time; it is evaluated at call-site invocation time based on 4 strict rules:
This Binding Precedence Order
`new` Binding
`new Constructor()` sets `this` to newly created instance
Explicit Binding
`call()`, `apply()`, or `bind()` forces target context
Implicit Binding
`obj.method()` sets `this` to the preceding object `obj`
Default Binding
Standalone call: `globalThis` in sloppy, `undefined` in strict
Losing `this` When Passing Methods as Callbacks
When you pass a method reference into a callback consumer like setTimeout, an event listener, or Array.prototype.map, you are passing only the raw function reference, stripping away its implicit object context:
Arrow Function Class Fields: Pros vs Cons (The Memory Tax)
Many React developers use arrow functions for all class methods: handleClick = () => { ... }. While convenient, interviewers look for candidates who understand the hidden architectural trade-offs:
Writing Your Own `Function.prototype.bind`
Can you implement a compliant bind polyfill that handles currying and respects the new operator override?
7. Tricky Output Predictions: The Interview Gauntlet
Battle-tested interview code puzzles. Test your mental model, inspect the exact console output, and uncover the specification reasons.
Interviewers rarely ask for textbook definitions. Instead, they present short snippets of JavaScript code and ask: "What gets logged to the console, and why?"
The Classic Prototype & Detached Method Puzzle
Think through the execution steps before checking
Mutating Prototype After Instantiation
Think through the execution steps before checking
Overwriting Child.prototype With Object Literal
Think through the execution steps before checking
Shared Mutable Reference on Prototype
Think through the execution steps before checking
`super` Inside Plain Object Literals
Think through the execution steps before checking
Class Getter vs Own Property Collision
Think through the execution steps before checking
8. Implement from Scratch: Coding Round Polyfills
Writing your own `new`, `Object.create`, `instanceof`, `call`/`apply`, EventEmitter, data structures, and design patterns.
In technical screening rounds for Tier-1 technology companies, you will often be asked to write polyfills without using built-in shortcuts:
Polyfill: `Object.create`
Polyfill: `instanceof` Operator
Polyfill: `Function.prototype.call` and `apply`
Data Structure: Custom `EventEmitter` Class
Data Structures: `Stack`, `Queue`, and `LinkedList`
Essential OOP Design Patterns (Singleton, Strategy, Observer)
Deep Clone Preserving Prototypes and Circular References
Standard structuredClone() strips away custom prototype chains and class methods. Here is how to clone deeply while preserving the prototype hierarchy and avoiding cyclic stack overflows:
9. SOLID Principles & Enterprise Design in JavaScript
Applying SOLID in dynamic environments, constructor dependency injection, Controller/Service/Repository layering, and when classes are the wrong tool.
How SOLID Principles Translate to JavaScript
SOLID is not exclusive to compiled languages like Java or C#. In modern full-stack Node.js and TypeScript architectures, SOLID provides the guardrails that prevent microservices and enterprise backends from degrading into unmaintainable spaghetti code:
Constructor Dependency Injection in Node.js
Hard-coding singleton imports directly inside service files (import db from './db') couples your business logic tightly to specific infrastructure, making unit testing almost impossible without brittle module mocks:
Controller / Service / Repository Layering
In modern Express, Fastify, or NestJS applications, the standard three-tier architecture separates concerns cleanly:
When Classes Are the WRONG Tool in JavaScript
Top interviewers respect candidates who know when not to use classes. JavaScript is a multi-paradigm language, and forcing classes everywhere is an anti-pattern:
When to Use Classes
- • Stateful domain entities (e.g.
Order,UserSession) - • Data structures with high instantiation volume (e.g.
LinkedList,Tree) - • Custom Error hierarchies (
class NotFoundError extends Error) - • Infrastructure services injected via DI (
PaymentService)
When Functions / Modules are Better
- • Pure utility calculations (use exported functions:
formatDate,calcTax) - • Data transformation pipelines (use
map,filter,reduce) - • React components (modern React standard is Hooks + Function Components)
- • Stateless middleware functions in Express
10. Node.js Backend OOP: Production Patterns
Operational error hierarchies, extending `EventEmitter`, Mongoose active record architecture, and Node module caching singletons.
Custom Error Hierarchies with `Error.captureStackTrace`
In production Node.js architectures, throwing generic string errors or plain new Error("failed") is a major liability. Senior backend engineers construct structured domain error hierarchies:
Subclassing `EventEmitter` in Real-Time Systems
Node.js is built on event-driven architecture. Creating custom event producers by extending EventEmitter is the standard pattern for background workers, stream aggregators, and WebSocket brokers:
How Mongoose & Express Relate to OOP
Even though Express handlers look like functional middleware pipelines, Node web frameworks rely on OOP underneath:
Module-Level Singletons vs Class Singletons
In Node.js, you often don't need a classic Java-style singleton class because Node's module loader caches modules in memory after the first import:
Conclusion & Interview Readiness Checklist
Quick reference summary matrix and the 10-point mastery checklist before your technical round.
Mastering Object-Oriented Programming in JavaScript is not about memorizing syntax; it is about understanding the underlying engine mechanics: how memory is allocated, how property lookup traverses the prototype chain, how call-sites determine lexical versus dynamic contexts, and how clean software architecture is maintained in real-world systems.
Final Advice for Senior Technical Rounds
When asked an OOP question in an interview, resist jumping straight into boilerplate code. First articulate the mental model: explain whether you are optimizing for prototype sharing or closures, call out edge cases like null prototypes, and demonstrate architectural awareness by discussing composition and dependency injection.
Interviewers hire engineers who understand why systems behave the way they do, not just those who can regurgitate syntax.
Frequently Asked Interview Questions
Quick, high-impact answers optimized for senior and staff-level technical screens.
Enjoyed this article?Leave a reaction
Tap multiple times to show love!



💬 Discussion