Home / Articles

JavaScript’s Real Blueprints: Prototypes, Delegation, and Functions

Why class is syntax sugar over prototypes, how delegation differs from classical inheritance, where this breaks, and what functional style buys in JavaScript.

Publisher: REACTAPP.TOP11 min read2593 words

This article is published in English.

Sources: The True Blueprints to JavaScript

Forget rigid object-oriented blueprints imported from other languages. JavaScript’s real architecture has long rested on functional programming and prototypal delegation. Fighting that grain is what makes the language feel broken; leaning into it is what makes it versatile.

For roughly a decade JavaScript has looked like an ordinary OOP language. You type class, extends, constructor, and super(), and if you squint it resembles Java or C#. That resemblance is a deliberate costume. Underneath, the runtime is doing something else entirely. The moment someone forgets the costume—especially someone who learned classical OOP first—is the moment “impossible” bugs appear. JavaScript was never classical. Seeing what sits under the syntax is the only way to trust what the surface promises.

The class Keyword Is a Costume

Before ECMAScript 2015 (ES6), JavaScript had no class keyword. Developers still wrote object-oriented-looking code with constructor functions and manual prototype assignment. When class arrived, it was introduced as syntax sugar: a friendlier spelling of the same mechanism, not a new inheritance model bolted on top.

TC39, the standards committee, was explicit. A JavaScript class is not a blueprint stamped into instances the way a Java class is. It is a function wearing a different hat.

A few lines make that visible:

class Dog {
  constructor(name) {
    this.name = name;
  }
  bark() {
    return `${this.name} says woof`;
  }
};
// Then just ask JS what Dog actually is.
console.log(typeof Dog);    // "function"

The bark() method does not live on an abstract class table—it lives on Dog.prototype, exactly where a pre-2015 method would live. Conceptually, every class still compiles down to a function plus a prototype object with methods attached. The keyword changed; the machine did not move.

That matters because classical expectations smuggle in guarantees JavaScript never offered: a fixed hierarchy resolved at compile time, private fields by default, and method dispatch that behaves the same no matter how a method is called. Under class syntax, JavaScript offers a live, mutable, runtime lookup chain that resembles those guarantees until a case appears where the resemblance ends.

That is not a design flaw to apologize for. It is a costume doing its job: letting a different creature move through a room built for something else. Trouble starts when the costume is treated as anatomy. The real model to learn is delegation, not classical inheritance.

What Delegation Actually Means

Prototypal delegation and classical inheritance sound similar and behave differently. In a classical language, a subclass structure is fixed at definition or compile time—an instance of Dog is structurally a stamped copy of what Animal defines.

In JavaScript, an object does not receive a copy. It receives a live reference to another object (its prototype). When a property is missing, the engine does not fail immediately; it walks the reference and asks there instead. That walk is delegation:

  • Not: “Contains a photocopy of the parent’s behavior”
  • Instead: “Ask the parent object when the answer is missing locally”

The lookup algorithm is precise: check the object’s own properties first; if absent, follow the internal [[Prototype]] link (exposed historically as __proto__, and properly via Object.getPrototypeOf()); repeat until the property is found or the chain ends at Object.prototype, whose prototype is null. It is a runtime chain of “ask the next object,” not a static hierarchy frozen once.

Because the prototype link is live and mutable—not a baked-in copy—you can add a method to a shared prototype after a thousand objects already exist and every one gains it immediately. They hold a pointer, not a photocopy. You can even reassign an object’s prototype at runtime with Object.setPrototypeOf(), and later lookups follow the new chain.

Classical languages cannot do that without recompilation. JavaScript’s model is not a weaker classical inheritance; it is a different mechanic flexible enough to imitate classical inheritance when desired.

Object.create() shows delegation without the costume:

const animal = {
  speak() {
    return '...';
  },
};
const dog = Object.create(animal);
// creates dog w/animal set directly as its pro

No constructor, no new, no hidden keyword acting “on your behalf.” dog.speak() works because lookup delegates to animal when dog has no own speak. That is the mechanism class and extends wrap in friendlier syntax.

Where the Illusion Breaks

Teams arriving from C# or Java often discover the break during event wiring or timer callbacks, not during happy-path method calls. The costume teaches “methods belong to instances,” so losing this feels like a language bug rather than a call-site rule. Documenting the call-site rule early prevents weeks of cargo-cult .bind(this) sprinkled without understanding.

Private-state pressure also shows up in libraries that export “safe” objects while still exposing mutable bags. If a consumer can reach in and flip a field, invariants that looked internal were only conventional. Prefer factory closures or # fields when the invariant must hold; prefer plain objects when the data is intentionally shared.

Simple tutorials keep the costume intact. Seams appear when this enters anything more complex than calling a method directly on a visible object. In JavaScript, this was never bound to “the instance” the classical way. Call-site mechanics decide the receiver—not the lexical home of the function, and not the class block where the method was written.

The crack almost everyone hits: pass a method as a callback:

setTimeout(instance.greet, 1000);
// or, wiring up an event listener directly
const el = document.getElementById('btn');
el.addEventListener('click', instance.handleClick);
// NOTE: The `this` inside that function is no longer the instance
// you extracted it from.

In a classical language such as C#, a bound method carries its instance with it. In JavaScript, a method is an ordinary function. Detached from the call site that supplies this, it loses the instance. setTimeout and DOM listeners are common places that detach happens. Fixes include .bind, arrow wrappers, or redesigning so the call site still provides the right receiver—none of which classical languages force you to think about for ordinary methods.

A quieter crack is real private state. Underscore conventions (this._internal) are social agreements, not enforcement. Modern #private fields close some of that gap, but they arrived late and still sit beside a culture of public mutable properties. Expecting Java-style encapsulation by default leads to surprise when outside code mutates what looked “internal.”

Rebuilding Class From Prototypes, By Hand

The fastest way to internalize the model is to rebuild the same behavior without class, using ordinary constructor functions plus prototype objects—the pre-2015 style the engine still uses under the sugar. The MDN documentation for core scripting functions and for advanced object prototypes remains a useful reference while practicing those patterns.

// function (using the `this` keyword)
function Dog(name) {
  this.name = name;
};

// prototype (doing the same thing)
Dog.prototype.bark = function() {
  return `${this.name} says woof`;
};

// assigning a variable to a new instance
const rex = new Dog('Rex');
console.log(rex.bark());
// Output: "Rex says woof"

Walking through new Dog('Rex') demystifies a keyword many people use for years without hearing the steps: create a new empty object; set that object’s [[Prototype]] to Dog.prototype; call Dog with this bound to the new object; return that object unless the constructor explicitly returns another object. There is no hidden class registry and no compiled blueprint—only object creation, a prototype link, and a function call with a particular this.

Inheritance without extends looks like this—the exact shape extends compiles toward:

// an extension of the Dog
function Puppy(name) {
  Dog.call(this, name);
};

Puppy.prototype = Object.create(Dog.prototype);
Puppy.prototype.constructor = Puppy;

// This line here doesn't work, because arrow functions can't use `this`.
// Puppy.prototype.bark = () => { return `${this.name} whines instead of barks`; };

// Use a standard function like this instead:
Puppy.prototype.bark = function() {
  return `${this.name} whines instead of barks`;
};

const buddy = new Puppy('Buddy');
console.log(buddy.bark());
// Output: "Buddy whines instead of barks"

Object.create(Dog.prototype) does what extends Dog automates: wire Puppy.prototype’s [[Prototype]] to Dog.prototype so missing properties delegate upward. Dog.call(this, name) does the work super(name) does: run the parent initializer against the same instance. Once that wiring is built by hand a few times, class stops being magic and becomes a thin spelling of a known mechanism.

Functions Were the Native Language All Along

Thinking in functions also clarifies module design. Exporting a handful of focused functions from a file is often clearer than exporting a class whose methods mostly ignore shared instance fields. When a “class” has no meaningful per-instance state, it was a namespace wearing OOP clothes. A module of functions says the same thing with less ceremony and fewer surprises about this.

Closures pair with that style: a factory can close over configuration and return a small API object whose methods do not need a classical hierarchy. The result still looks object-oriented at the call site (api.save()), while the implementation stays lexical rather than prototype-based when that is a better fit.

After the classical costume looks optional, a sharper question appears:

What does JavaScript’s actual native grain look like underneath?

The answer is functions as first-class values—passable, storable, returnable—not class hierarchies. Functions in that sense were present from JavaScript’s first release in 1995. Classes arrived about twenty years later. Most of the language’s history had little to do with OOP. If you hunt for the feature JavaScript was built around, it was never the object hierarchy. It was the function.

Historical influences often cited for that grain include:

  • Scheme — a Lisp-family language emphasizing first-class functions
  • Self — a prototype-based language that shaped the delegation model
  • Java — a surface syntax audience that later made the class costume commercially attractive

Closures follow naturally from first-class functions: a function retains access to the lexical environment where it was created. That retention is how factories hide state without classical private fields, and how hooks and middleware keep per-request context without subclasses.

What Functional Programming Actually Buys You

Practically, teams adopt the habits piecemeal. Keep input and output at the edges—network, disk, DOM—and keep the middle of the program as transformations of plain data. That split alone removes many race conditions that appear when UI events mutate the same object graph from multiple handlers.

Immutability helpers (spreads, structuredClone, or libraries) are tactics; the strategy is making illegal intermediate states harder to express. Discriminated unions and pure update functions pair well with the prototype lessons above: objects remain useful, but update paths become explicit values rather than silent field writes.

Debugging also changes shape. With pure updates you can log inputs and outputs of a transform and reproduce failures offline. With tangled instance mutation you often need a full app session to recreate the exact field soup that triggered a bug.

Strip the academic framing and functional programming, in practical terms, means building programs primarily from pure functions: same input, same output, no side effects outside the return value. Programs compose those functions into pipelines of transformations, rather than mutating instance fields through methods over time.

The first payoff is testability. A pure function needs no setup/teardown theater, no mocked this, and no carefully staged instance. Feed an input, assert an output. Much of what makes OOP tests flaky is the ceremony required to push an object into the right internal state before exercising the behavior that matters. Pure functions skip that ceremony because there is no hidden instance status to stage first.

The second payoff answers the this section directly: shared mutable state is the root of most classic binding headaches. A method that reads and writes shared instance fields can be affected by anything else that touched the instance, in an order the call site does not make obvious. A pure function has no shared mutable bag, so reordering callers does not silently change meaning.

The third payoff is composability. Tiny pure helpers chain well: each return value feeds the next step, forming pipelines that read downward and admit per-step tests. Tangled class hierarchies resist that recombination because behavior is smeared across inheritance chains and mutable fields instead of living in independently testable units.

None of this bans objects or methods. It reframes when they help: as boundaries and namespaces, not as the default place every piece of behavior must live.

Final Point

Teaching this model to newcomers shortens the time between copying a class tutorial and understanding why a click handler lost its receiver. It also softens religious wars: classical patterns remain available as syntax sugar when they clarify domain boundaries, while daily array pipelines, reducers, and hooks keep the language’s native grain visible.

When reviewing code, ask whether a new class is modeling a real identity with lifespan and invariants, or merely grouping functions. If it is the latter, a module of functions—or a single factory—usually communicates better. If it is the former, keep the class, but remember it still sits on prototypes and call-site this, and write tests that exercise detached callbacks when those call sites exist in production.

None of this is theoretical in day-to-day JavaScript. Array methods such as map, filter, and reduce are functional tools and among the most reached-for APIs in the language. React’s move from class components to function components and Hooks—starting with React 16.8, and continuing through later major releases—was a deliberate shift toward treating a component as a pure function of props and state, which is easier to reason about than a class juggling lifecycle methods and mutable fields.

Redux reducers follow a purity contract spelled out in the official fundamentals tutorial on state, actions, and reducers: identical state and action inputs must yield the same next state without mutating the previous one in place—which is why time-travel debugging is possible. See the Redux fundamentals guide for the canonical wording. Many teams also prefer lighter stores such as Zustand for application state; the same purity habits still apply wherever updates must stay predictable.

Stream and event-processing pipelines—libraries that compose small transforms over ongoing event sequences—are functional programming applied to one of frontend’s messiest problems: asynchronous, continuous user input.

The takeaway: JavaScript was never classical. OOP patterns can still help at boundaries, but the native blueprints were always functional. For two decades the language wore a costume that made it look otherwise—convincing enough that a generation learned through the costume, bugs included. Underneath, it remained a language of functions and delegation—closer to Scheme in a Java jacket than to Java itself—which means functional style in JavaScript was never a foreign import. It was the native accent all along.

Put differently: learn the costume so collaboration stays easy, learn the anatomy so production bugs make sense. Teams that only memorize class syntax keep rediscovering detached callbacks and surprising prototype mutations. Teams that also practice Object.create, constructor functions, and pure updates spend less time arguing with the runtime and more time shaping domain data.

With that foundation, class becomes an optional dialect rather than the definition of the language—and debugging starts from call sites and prototype links instead of from assumptions imported from classical runtimes.

Reviewers who ask for that distinction early prevent accidental god-objects dressed as domain models.