---
title: "Prototypes, Not Classes: What's Actually Happening in JavaScript"
description: "How JavaScript's prototype chain works, how ES6 classes map to it, what __proto__, prototype, and [[Prototype]] actually mean, and a real inheritance bug."
url: https://ashishlekhyani.com/articles/prototypes-not-classes/
publishDate: 2026-09-08
tags: ["javascript", "prototypes", "classes", "fundamentals"]
---

Let's be straight about it, JavaScript is a prototype based language, not a class based one. And since you're here, you probably already know this much. You've written `class Animal {}` a hundred times, and it looks like a class, works like a class, and everyone calls it a class. But it's not completely true.

Think of any well known actor who's spent years playing a character. Audiences know the character's name, quote the lines, recognize the character's face. But behind every scene, it was the actor doing the actual work, performing as the character, delivering the line, making the choices in the moment. The character gets the fame. The actor does the job.

That's classes and prototypes. The class is the character, the name everyone knows, the thing you write, the thing that gets addressed. The prototype is the actor, the one actually doing the work underneath, whether or not the audience ever learns their name.

And there was a time when the actor had no character. That's JavaScript before ES6, when prototypes were the only way to do inheritance, no class syntax. Then in 2015, JavaScript wrote a character to play for them. `class`, not a new actor, not a new set of skills, just a name with a costume that made them easier to work alongside languages like Java and C++.


## The illusion, proven in one line

Here's the entire argument, compressed into a few lines of code:

```javascript
class Wolf {}

console.log(typeof Wolf); // "function"
```

Not `"class"`. Not some special new type. `"function"`. JavaScript doesn't consider it a fundamentally new thing, it's a function underneath, dressed up in different syntax. That's the mask coming off.

So if `class` is really just a function with different syntax dressed on top, what's actually happening when you use one? To understand that, we need to go back to how JavaScript worked *before* `class` existed, because that mechanism never went away. It's still there, exactly as it was before 2015.

## What a prototype actually is

Every object in JavaScript has an internal link to another object, its prototype. When you try to access a property or method on an object and it's not found directly on that object, JavaScript walks up to the prototype of that object and looks there. If it's not there either, it keeps walking up, until it either finds the property or reaches the very end of the chain: `null`.

This is the **prototype chain**, and it's the mechanism behind everything you've ever called "inheritance" in JavaScript.

```javascript
const animal = {
  eats: true,
};

const kangaroo = {
  jumps: true,
};

kangaroo.__proto__ = animal; // illustrating the link

console.log(kangaroo.jumps); // true, own property
console.log(kangaroo.eats);  // true, found via the prototype
```

`kangaroo` doesn't have `eats` written on it anywhere. JavaScript checked `kangaroo` first, didn't find it, walked up to `animal`, found it there, and handed it back. That’s basically the mechanism. The rest is mostly details built around it.

### Why this exists at all

Before this syntax mattered, JavaScript needed a way to share things across many objects without physically copying those into every single one of them. Think about how many arrays exist in a single running program: thousands, sometimes millions. Each one does *not* carry its own private copy of `.push`, `.map`, `.filter`. There's exactly one set of those methods, living on `Array.prototype`, and every array in existence just points to it. That's not just implementation, it's the reason the language works the way it does, memory wise and functionally.

## `prototype`, `__proto__`, and `[[Prototype]]`: same idea, three names

This is where I need to be precise to make it clear.

**`[[Prototype]]`** is the real thing: an internal place, defined in the JavaScript spec itself, that exists on *every* object (and functions are objects too, so they have one as well). The double brackets in the spec mean "this is something tracked internally, not exposed to your code."

**`__proto__`** is just a doorway into `[[Prototype]]`. It's a getter and setter: reading `obj.__proto__` reads `[[Prototype]]`, writing to it writes `[[Prototype]]`. It's the old way, the modern way is `Object.getPrototypeOf(obj)` and `Object.setPrototypeOf(obj, x)`.

**`.prototype`** is a different thing altogether. It's a normal property sitting on the function, nothing internal, nothing hidden. JavaScript creates it automatically when you write `function Wolf() {}` or `class Wolf {}`. Arrow functions don't get one, even though `typeof` still reports `"function"`. Neither do bound functions, or most methods.

And it is not "what `Wolf` itself inherits from." That's still `Wolf`'s own `[[Prototype]]`, same as any other object. `.prototype` is a template. When you later write `new Wolf()`, the new object gets linked to that template.

```javascript
function Wolf() {}

console.log(Wolf.prototype);
// { constructor: Wolf }
```

That's it, one property by default, `constructor`, pointing back to `Wolf`. Everything else you see on `Wolf.prototype` later is stuff you add yourself.

Here's the table version, since this is worth seeing side by side:

<table>
  <thead>
    <tr>
      <th scope="col">Question</th>
      <th scope="col"><code>{'Wolf.__proto__'}</code> (→ <code>{'[[Prototype]]'}</code>)</th>
      <th scope="col"><code>Wolf.prototype</code></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th scope="row">What it is</th>
      <td>What <code>Wolf</code> itself inherits from</td>
      <td>The template future instances of <code>Wolf</code> inherit from</td>
    </tr>
    <tr>
      <th scope="row">Points to</th>
      <td><code>Function.prototype</code></td>
      <td>An ordinary object, <code>{'{ constructor: Wolf }'}</code> by default</td>
    </tr>
    <tr>
      <th scope="row">Exists on</th>
      <td>Every object, including functions</td>
      <td>Constructor functions and classes, not arrow functions</td>
    </tr>
  </tbody>
</table>

This is where the two usually get connected, when you use `new`.

```javascript
function Wolf(name) { this.name = name; }
const rex = new Wolf("Rex");

rex.__proto__ === Wolf.prototype; // true
```

`rex`'s `[[Prototype]]` now points at `Wolf.prototype`. Two separate concepts, one connecting event.

## What `new` actually does

When you write `new Wolf("Rex")`, JavaScript runs through four steps, every time:

1. Create a brand-new, empty object.
2. Set that new object's `[[Prototype]]` to `Wolf.prototype`.
3. Call `Wolf` with `this` bound to the new object.
4. Return the new object, unless `Wolf` explicitly returns some other object itself.

You can write this out by hand, and it behaves identically:

```javascript
function myNew(Constructor, ...args) {
  // Create a new, empty object, and link its [[Prototype]]
  // to Constructor.prototype — this replicates step 1 and 2 of `new`
  const obj = Object.create(Constructor.prototype);

  // Run the constructor function, forcing `this` inside it
  // to be our new object — this replicates step 3 of `new`
  const result = Constructor.apply(obj, args);

  // If the constructor explicitly returned its own object, use that.
  // Otherwise, fall back to the object we built above — this is step 4
  return result instanceof Object ? result : obj;
}
```

Worth knowing: any function can be used this way. JavaScript doesn't have a separate "constructor" type, "constructor" describes how a function is *used*, not what it fundamentally is.

```javascript
function Wolf(name) { this.name = name; }
Wolf("Rex");      // without new: this becomes the global object, or throws a TypeError
new Wolf("Rex");  // used as a constructor, actually builds an object
```

Same function, two different outcomes, depending entirely on whether `new` shows up. Capitalizing `Wolf` is just a convention telling other developers "use `new` with me", the language itself doesn't enforce it for regular functions. `class` does enforce it, and that's one of the real, non-cosmetic things it adds:

```javascript
class Wolf {}
Wolf(); // TypeError: Class constructor Wolf cannot be invoked without 'new'
```

## Desugaring `class`: what it actually compiles down to

Here's the full side-by-side, since this is the centerpiece of the whole argument:

```javascript
// What you write
class Animal {
  constructor(name) { this.name = name; }
  speak() { console.log(`${this.name} makes a noise`); }
  static create(name) { return new Animal(name); }
}

// What it actually is, underneath
function Animal(name) { this.name = name; }
Animal.prototype.speak = function () {
  console.log(`${this.name} makes a noise`);
};
Animal.create = function (name) { return new Animal(name); };
```

Notice where `static` is stored, directly on `Animal` itself, not on `Animal.prototype`. That's because, static methods aren't meant to be inherited by instances, only used by the class itself (or by a subclass built on top of it), so they live on the constructor function, not the shared instance template.

A few things `class` genuinely adds, beyond just being a nicer way to write the same thing:

- Calling a class without `new` throws an error, as shown above. Calling a constructor function without `new` is also unsafe. In older, non-strict scripts, `this` quietly becomes the global object (`window` in a browser). In modern code — modules, or anything with `"use strict"` — assigning `this.name` throws a `TypeError`. Either way, you don't get the object you meant to build.
- Class methods are **non-enumerable** by default. Methods you attach the old way (`Animal.prototype.speak = ...`) are enumerable by default, meaning they'd show up in a `for...in` loop over an instance unless you deliberately hide them.
- Classes live in the 'temporal dead zone': you can't use them before the line they're defined on, no hoisting, unlike `function` declarations.

So `class` isn't purely cosmetic, it adds real guardrails. But underneath, it's the same mechanism doing the same work, through the same chain.

## Multi-level inheritance: the old way and the class way

```javascript
// The old way

function Animal(name) {
  this.name = name;
}
Animal.prototype.eat = function () {
  console.log(`${this.name} eats`);
};

function Wolf(name) {					// borrow the constructor
  Animal.call(this, name);
}
Wolf.prototype = Object.create(Animal.prototype);	// link the prototypes
Wolf.prototype.constructor = Wolf;			// repair the constructor reference
Wolf.prototype.howl = function () {
  console.log(`${this.name} howls`);
};
```

Three things are happening here, and each one maps to something `extends` keyword later automated.

**`Animal.call(this, name)`**: this is constructor stealing. It runs `Animal`'s setup logic, but forces `this` inside `Animal` to be the *new Wolf* being built, instead of a separate Animal object. Without it, you'd have to duplicate `this.name = name` inside `Wolf` yourself.

**`Wolf.prototype = Object.create(Animal.prototype)`**: this is the actual inheritance wiring. `Object.create(Animal.prototype)` builds a brand-new, empty object whose `[[Prototype]]` points at `Animal.prototype`, and that becomes `Wolf.prototype`. The chain now reads: `Wolf.prototype → Animal.prototype → Object.prototype → null`. This is *not* the same as writing `Wolf.prototype = Animal.prototype` directly. That would make them the literal same object in memory, meaning anything you later add to `Wolf.prototype` would leak straight onto `Animal.prototype` too, and every Animal would suddenly know how to howl.

**`Wolf.prototype.constructor = Wolf`**: a repair job. The line above replaced `Wolf.prototype` with a brand-new object that has no `constructor` property of its own, so a lookup for `Wolf.prototype.constructor` walks the chain and incorrectly lands on `Animal`. This line manually points it back to `Wolf`.

Here's the same thing, written with `class`:

```javascript
class Animal {
  constructor(name) { this.name = name; }
  eat() { console.log(`${this.name} eats`); }
}

class Wolf extends Animal {
  howl() { console.log(`${this.name} howls`); }
}

const rex = new Wolf("Rex");
rex.eat();  // "Rex eats", inherited
rex.howl(); // "Rex howls", its own
```

`extends` does all three of those manual steps invisibly. That's genuinely useful, but it's worth knowing what it's hiding, because when something breaks, JavaScript won't tell you it's because of what `extends` did behind the scenes.

## The bug this actually causes

Here's a version of a mistake I ran into while learning about prototypes myself. It took me two hours to figure out, and it's easy to make without ever touching `class`:

```javascript
function Wolf() {}
Wolf.prototype = { howl() { console.log("awoo"); } }; // replaced the whole object

const rex = new Wolf();

console.log(rex.constructor === Wolf);    // false!
console.log(rex.constructor === Object); // true, not what you'd expect
```

Overwriting `Wolf.prototype` entirely, instead of adding to the existing one, throws away the auto-generated `{ constructor: Wolf }` object and replaces it with a fresh object that has no `constructor` of its own. `rex instanceof Wolf` still returns true, because `instanceof` walks the `[[Prototype]]` chain, not the `constructor` property. What breaks is anything that *reads* `.constructor`: debugging output, some serializers, checks like `obj.constructor === Wolf`. Nothing throws. Nothing warns you. It just silently reports the wrong constructor, possibly several files away from where the actual mistake was made.

The fix is either to add to the existing prototype object instead of replacing it (`Wolf.prototype.howl = ...`), or, if you do replace it, to manually restore `constructor` the same way the manual inheritance pattern does.

## The mixin pattern: a workaround built entirely on this mechanism

JavaScript only allows a single parent per object through the chain: no multiple inheritance. To get around that, a common pattern is to copy methods from several sources directly onto one prototype:

```javascript
const swimmer = {
  swim() { console.log(`${this.name} swims`); }
};
const flyer = {
  fly() { console.log(`${this.name} flies`); }
};

class Duck {
  constructor(name) { this.name = name; }
}
Object.assign(Duck.prototype, swimmer, flyer);

const donald = new Duck("Donald");
donald.swim(); // "Donald swims"
donald.fly();  // "Donald flies"
```

This works precisely because `Duck.prototype` is just an ordinary object: you can attach whatever you want to it, from wherever you want, at any point.

## When the mechanism turns dangerous: prototype pollution

Since nearly every plain object in a running program eventually chains up to `Object.prototype`, mutating it affects *everything*:

```javascript
const obj = {};
Object.prototype.isAdmin = true; // never do this

console.log(obj.isAdmin); // true, leaked onto every plain object in the program
console.log({}.isAdmin);  // true
```

This is a real, named security vulnerability, called prototype pollution, and it has shown up as actual CVEs (publicly tracked, official records of known security flaws) in widely used libraries. It's usually exploited through merge or clone functions that don't check their inputs, letting an attacker sneak in a key like `__proto__.isAdmin` and gain elevated permissions across an entire application. This isn't a trivia footnote, it's proof that the mechanism underneath `class` has consequences well past syntax preference.

## Where this leaves you

`class` didn't replace anything. It didn't introduce a new inheritance model, a new type system, or anything new. It gave an old mechanism a more familiar face, one that looks like Java, like C++ and Python.

It's worth knowing, the next time a `constructor` reference points somewhere strange, or a merge function starts behaving like it's reading your mind, that there's no class actually running any of it. There's a chain of objects, quietly checking each other, one link at a time, exactly the way it always has been.
