Technology  /  JavaScript

๐ŸŸจ JavaScript 15 guides ยท updated 2026

Closures, the event loop, promises, and modern ES syntax โ€” the language of the web explained the way it actually behaves at runtime.

JavaScript Prototypes and Classes: What class Syntax Actually Does

JavaScriptโ€™s class syntax looks reassuringly familiar to anyone from Java or C# โ€” and that familiarity is a well-designed veneer. Underneath, JS has no classes in the classical sense. It has objects linked to other objects, with a lookup rule. Thatโ€™s the entire inheritance system: the prototype chain.

You can write productive JS using only class syntax and never look underneath. But the seams show constantly โ€” in debugging (__proto__ in every console output), in interviews (a top-five question), in library code, and in error messages. This guide teaches the real model first, then shows how class maps onto it โ€” after which both layers are simple.


The Core Mechanism: Delegated Property Lookup

Every JavaScript object has a hidden internal link to another object โ€” its prototype (or to null, ending the chain). Property lookup follows one rule:

Read obj.x: if obj has its own property x, use it. Otherwise, look at objโ€™s prototype. Repeat up the chain until found or the chain ends (undefined).

const baseConfig = { retries: 3, timeout: 5000 };
const jobConfig = Object.create(baseConfig); // creates {} with prototype = baseConfig
jobConfig.timeout = 30_000; // own property, shadows the prototype's
jobConfig.timeout // 30000 โ€” found on jobConfig itself
jobConfig.retries // 3 โ€” not own; found one link up, on baseConfig
jobConfig.missing // undefined โ€” chain exhausted
Object.getPrototypeOf(jobConfig) === baseConfig // true
jobConfig.hasOwnProperty("retries") // false โ€” it's inherited, not own

Notice what this is: not copying, not classical inheritance โ€” live delegation. jobConfig doesnโ€™t contain retries; it defers to an object that does. Change baseConfig.retries and every object delegating to it sees the new value instantly. And writing jobConfig.retries = 5 doesnโ€™t touch baseConfig โ€” assignment creates an own property that shadows the inherited one (a frequent source of โ€œinheritanceโ€ confusion: reads climb the chain; writes donโ€™t).

This rule already explains things youโ€™ve used all along:

const arr = [1, 2, 3];
arr.map(x => x * 2); // arr has no 'map' of its own...
// arr โ†’ Array.prototype (map, filter, push live HERE, once, shared) โ†’ Object.prototype โ†’ null
"abc".toUpperCase(); // same story via String.prototype
({}).toString(); // Object.prototype.toString โ€” the chain's last stop

Every array shares one map function, living on Array.prototype. A million arrays, one method โ€” thatโ€™s the memory model prototypes exist for. Itโ€™s also why โ€œmonkey-patchingโ€ (Array.prototype.myHelper = ...) affects every array in the program โ€” powerful, and banned by most style guides for good reason.


What new Actually Does

Before class, constructors were plain functions used with new. Understanding new is understanding the whole system โ€” it does four things:

function Account(owner) { // just a function
this.owner = owner; // (2) this = the new object
this.paise = 0;
}
Account.prototype.deposit = function (p) { // methods go on the prototype, shared
this.paise += p;
};
const a = new Account("Priya");

new Account("Priya"):

  1. Creates a fresh empty object.
  2. Sets its prototype link to Account.prototype.
  3. Runs the function body with this bound to the new object (Rule 4).
  4. Returns the object (unless the body explicitly returns another object โ€” a quirk, not a tool).

So a gets own data (owner, paise) and delegated behavior (deposit, found via the chain):

a { owner: "Priya", paise: 0 }
โŸถ Account.prototype { deposit }
โŸถ Object.prototype { toString, hasOwnProperty, ... }
โŸถ null

instanceof is a chain question: a instanceof Account asks โ€œdoes Account.prototype appear anywhere in aโ€™s chain?โ€ โ€” nothing more mystical than that.


class: The Same Machine, Humane Syntax

ES2015โ€™s class is (almost entirely) syntax over the constructor-function pattern โ€” deliberate, standardized sugar:

class Account {
#paise = 0; // private field (ES2022) โ€” truly inaccessible outside
constructor(owner) {
this.owner = owner;
}
deposit(p) { // โ†’ goes on Account.prototype, shared
if (p <= 0) throw new Error("deposit must be positive");
this.#paise += p;
}
get balance() { // getter โ€” read as a.balance, no parens
return this.#paise;
}
static fromJSON(json) { // โ†’ on Account itself, not instances
const acc = new Account(json.owner);
acc.deposit(json.paise);
return acc;
}
}
const a = new Account("Priya");
a.deposit(50_000);
a.balance; // 50000
a.#paise; // SyntaxError โ€” privacy enforced by the language

Mapping to the machine: deposit lands on Account.prototype exactly as the manual version did; static members land on the constructor function itself (compare Javaโ€™s static); new performs the same four steps. What class adds beyond sugar: required new (calling without it throws instead of corrupting globals), always-strict bodies, #private fields with real enforcement (closing the gap closures used to fill alone), and saner inheritance syntax:

class SavingsAccount extends Account {
constructor(owner, rate) {
super(owner); // parent constructor first โ€” same rule as Java
this.rate = rate;
}
addInterest() {
this.deposit(Math.round(this.balance * this.rate)); // inherited API
}
}

extends wires two chains: instances go savings โ†’ SavingsAccount.prototype โ†’ Account.prototype โ†’ Object.prototype, and method lookup walks it โ€” which is all โ€œmethod overridingโ€ is in JS: a nearer property shadowing a farther one. super.method() continues the lookup one level up, explicitly.

Guidance that reflects the modern ecosystem: write class when you want class-shaped things โ€” many instances, shared methods, real instanceof identity (errors, domain entities, framework integration). Donโ€™t reach for it reflexively โ€” plain objects, factory functions with closures, and modules cover a great deal of what other languages need classes for, and much of the JS world (React included) has drifted toward functions. Both are idiomatic; class-hierarchies-for-everything is not. And the composition-over-inheritance caution from the Java series applies with extra force in JS: keep extends chains shallow (one level, usually), or flatten them into composition.


Prototype Literacy for Debugging and Interviews

The bits that separate โ€œuses classesโ€ from โ€œunderstands the systemโ€:

// The three prototype accessors
Object.getPrototypeOf(a) // the real API for reading the link
a.__proto__ // legacy accessor โ€” fine in DevTools, avoid in code
Account.prototype // property of the CONSTRUCTOR: "what instances will link to"
// __proto__ (instance's link) vs .prototype (constructor's template) โ€” the naming
// collision behind 90% of prototype confusion. They meet only via new.
// Own vs inherited iteration
Object.keys(a) // own enumerable only: ["owner"]
for (const k in a) {} // own AND inherited enumerable โ€” usually not what you want
Object.hasOwn(a, "deposit") // false โ€” modern replacement for hasOwnProperty
// Objects without any prototype
const dict = Object.create(null); // no toString, no hasOwnProperty โ€” a pure dictionary

That last one explains a pattern youโ€™ll meet in libraries: Object.create(null) makes collision-proof dictionaries (no inherited constructor key lurking). Though in modern code, Map is usually the better dictionary anyway.

Console literacy: when DevTools shows an object, the expandable [[Prototype]] (or __proto__) entry is the chain โ€” walk it and youโ€™re reading the objectโ€™s ancestry directly. Seeing a method there, not on the object itself, is normal and correct.


Worked Pattern: A Custom Error Family

The one inheritance hierarchy nearly every codebase should have โ€” and a compact tour of this chapterโ€™s machinery in production shape:

class AppError extends Error {
constructor(message, options) {
super(message, options); // Error supports { cause } โ€” keep the chain
this.name = this.constructor.name; // subclasses get their own name automatically
}
}
class NotFoundError extends AppError {
constructor(resource, id) {
super(`${resource} ${id} not found`);
this.resource = resource;
this.id = id;
}
}
class ValidationError extends AppError {
constructor(issues) {
super(`Validation failed: ${issues.length} issue(s)`);
this.issues = issues;
}
}
// throwing and routing:
throw new NotFoundError("Order", 42);
try { await save(order); }
catch (e) {
if (e instanceof ValidationError) return showFieldErrors(e.issues);
if (e instanceof NotFoundError) return show404();
throw e; // bugs keep propagating โ€” never swallow
}

Everything from this guide is load-bearing here: extends Error wires the prototype chain so instanceof routing works; super(message) runs the parent constructor; this.constructor.name reads through the chain to the actual subclass; own properties (issues, resource) carry structured data that string matching never could. The error-design principles from the Java series โ€” small family, structured data, narrow catches, preserve causes โ€” translate wholesale; only the syntax changed. If you build one class hierarchy this year, make it this one.

Frequently Asked Questions

So does JavaScript have โ€œrealโ€ classes now or not? The syntax is real and standard; the semantics remain prototypal delegation. The distinction matters rarely โ€” but it matters: prototypes are live (modifying Account.prototype.deposit changes behavior of existing instances โ€” try that in Java), lookup is dynamic, and objects can be linked without any constructor at all. โ€œSugarโ€ undersells class; โ€œJava classesโ€ oversells it.

When do I actually need Object.create or manual prototypes? Almost never in application code โ€” class and object literals cover it. The manual layer surfaces in library internals, polyfills, certain performance-sensitive patterns, and interviews. The reading knowledge is what you need.

Whatโ€™s the constructor property? a.constructor walks the chain to Account.prototype.constructor, which points back at Account. Occasionally useful (a.constructor.name for logging), historically fragile (itโ€™s just a writable property). Prefer instanceof for identity checks.

How do private fields (#) differ from the closure pattern and TypeScriptโ€™s private? #field is enforced by the engine โ€” invisible outside, full stop, and even subclasses canโ€™t see it. Closure privacy is equally airtight, function-shaped instead. TypeScriptโ€™s private keyword is compile-time advice โ€” erased at runtime, bypassable with bracket access. For real runtime privacy: # or closures.

Why does extends from built-ins like Array sometimes behave oddly? Subclassing built-ins (Array, Error) works in modern engines but carries edge cases (method return types, transpilation quirks with older targets). Extending Error for custom error types is the one broadly recommended case โ€” as in Java, a small family of domain errors pays off; deep built-in subclassing generally doesnโ€™t.


Mixins: Multiple Inheritanceโ€™s JS Workaround

Since JS classes extend only one parent, shared cross-cutting behavior (serialization, event emitting, timestamps) uses mixins โ€” functions that take a class and return an extended one:

const Serializable = (Base) => class extends Base {
toJSON() { return { ...this }; }
};
const Timestamped = (Base) => class extends Base {
createdAt = new Date();
};
class Order extends Serializable(Timestamped(Object)) { /* ... */ }

Each mixin inserts a link into the prototype chain โ€” Order โ†’ Serializable's class โ†’ Timestamped's class โ†’ Object โ€” so lookup finds every layerโ€™s methods. Itโ€™s a clever pattern worth reading fluently (several libraries use it), and worth writing rarely: plain composition (a serializer utility taking any object, a closure-based helper) usually delivers the same reuse with less chain magic. The deeper takeaway is the mechanism: because classes are values and extends accepts any expression, JS can build inheritance hierarchies dynamically โ€” something classical languages canโ€™t โ€” which is prototypes showing through the class syntax one more time.

Self-Check

class A { greet() { return "a"; } }
class B extends A { greet() { return "b" + super.greet(); } }
const b = new B();
// 1
b.greet();
// 2
Object.hasOwn(b, "greet");
// 3
b instanceof A;
// 4 โ€” what does this do to EXISTING instances?
A.prototype.greet = function () { return "z"; };
b.greet();
// 5
const plain = { x: 1 };
Object.getPrototypeOf(plain) === Object.prototype;

Answers: (1) "ba" โ€” Bโ€™s method shadows, super continues lookup upward. (2) false โ€” methods live on B.prototype, not the instance. (3) true โ€” A.prototype is in bโ€™s chain. (4) "bz" โ€” prototypes are live; super.greet() now finds the replaced function. If that surprised you, reread delegation โ€” nothing was copied anywhere. (5) true โ€” literals link to Object.prototype.

Next: the chapter that explains why your console.log order keeps surprising you โ€” The Event Loop, JavaScriptโ€™s concurrency model and the foundation under every promise and callback in the language.