JavaScript’s Prototype Problem

JavaScript’s name may have been borrowed from Java, but the language itself shares far more DNA with Lisp and Scheme. Its prototypal inheritance model comes from the Self language, and this mechanism remains one of the least understood parts of the language. Developers can write JavaScript for years without ever touching a prototype — a feature that was both a deliberate design decision and a consequence of how the language was marketed.

The Two Flavors of OOP

Classical object-oriented programming is built around the idea of classes as blueprints and instances created from them. A class defines the structure and behavior of its objects, encapsulates properties and methods, and supports inheritance: one class takes on the properties and methods of another, enabling code reuse and hierarchies.

Diagram showing one class with two objects connected to another class connected to two more objects.
Diagram showing one class with two objects connected to another class connected to two more objects. (Large preview)

Prototypal OOP works differently. There are no classes — only objects — and new objects are created directly from existing ones. Every object has a built-in prototype property referencing its “parent” object’s prototype, which is why any array can call methods like .sort() or .forEach(): each array inherits from Array.prototype.

Prototypes are themselves objects with their own prototypes, forming the prototype chain. When you access a property, JavaScript looks for it on the object first, then walks up the chain until it finds the property or reaches the top of the chain, often ending at Object.prototype, whose prototype is null.

One crucial difference: in classical OOP, you can’t alter a class definition after instances exist. With JavaScript prototypes, you can add, delete, or change methods and properties on a prototype, affecting every object down the chain.

“Objects inherit from objects. What could be more object-oriented than that?”

Douglas Crockford
Diagram showing the flow between two objects that are each connected to two other objects
Diagram showing the flow between two objects that are each connected to two other objects. (Large preview)

The Syntactic Sugar Illusion

On paper, classical and prototypal OOP are fundamentally different. In JavaScript, however, the two approaches are identical beyond syntax. Compare the following code excerpts:

// With Classes

class Dog {
  constructor(name, color) {
    this.name = name;

    this.color = color;
  }

  bark() {
    return `I am a ${this.color} dog and my name is ${this.name}.`;
  }
}

const myDog = new Dog("Charlie", "brown");

console.log(myDog.name); // Charlie

console.log(myDog.bark()); // I am a brown dog and my name is Charlie.
// With Prototypes

function Dog(name, color) {
  this.name = name;

  this.color = color;
}

Dog.prototype.bark = function () {
  return `I am a ${this.color} dog and my name is ${this.name}.`;
};

const myDog = new Dog("Charlie", "brown");

console.log(myDog.name); // Charlie

console.log(myDog.bark()); // I am a brown dog and my name is Charlie.

These produce identical execution results, but the second example reflects what JavaScript actually does under the hood, while the first hides it behind what looks like familiar class-based syntax.

Is the classical syntax a problem? It has a valid argument in its favor: keeping all class-related code inside a block improves readability. The counterargument is stronger, though. The syntax is misleading and has led thousands of developers to believe JavaScript has true classes when a class is no different from any other function object.

The concern isn’t pretending classes exist — prototypes clearly do not behave like true class-based inheritance.

Consider this example:

class Dog {
  constructor(name, color) {
    this.name = name;

    this.color = color;
  }

  bark() {
    return `I am a ${this.color} dog and my name is ${this.name}.`;
  }
}

const myDog = new Dog("Charlie", "brown");

Dog.prototype.bark = function () {
  return "I am really just another object with a prototype!";
};

console.log(myDog.bark()); // I am really just another object with a prototype!"

The code accesses a class’s prototype. That works because classes don’t exist. They are constructor functions returning objects, and like any function, they carry a .prototype property.

The question is why JavaScript tries so hard to hide its prototypes.

A Language Shaped by Marketing

In May 1995, Netscape brought Brendan Eich on board to implement a scripting language for its browser. The original plan was to use Scheme — minimal and elegant. All that changed when Netscape partnered with Sun Microsystems to bring Java to the web. Eich and Sun founder Bill Joy saw an opportunity: a language approachable for designers, yet familiar enough for Java developers.

Eich built JavaScript in ten days, initially calling it Mocha, then LiveScript. In December 1995, the language was renamed JavaScript to ride Java’s coattails. The language’s prototype-based nature stayed, but the pressure to look like Java shaped its public face:

“I was thinking the whole time, what should the language be like? Should it be easy to use? Might the syntax even be more like natural language? [...] Well, I’d like to do that, but my management said, ‘Make it look like Java.’”

The core design was a blend Eich himself defended:

“I’m not proud, but I’m happy that I chose Scheme-ish first-class functions and Self-ish prototypes as the main ingredients.”

The tension — prototypes under a Java facade — was both a matter of marketing and practicality. Eich later reflected:

“It is ironic that JS could not have class in 1995 because it would have rivaled Java. It was constrained by both time and a sidekick role.”

JavaScript became the world’s most widely used prototype-based language, but its prototypal heart was obscured by the syntax it had to wear to satisfy its corporate parent.

Prototypes: JavaScript's Unfinished Feature

Douglas Crockford's JavaScript: The Good Parts, published in 2008, criticized JavaScript for borrowing too much from Java while neglecting its own prototype-based strengths. Revisiting those critiques today — with the benefit of modern JavaScript improvements — reveals a recurring pattern: language features designed to mimic classical OOP introduced complexity that could have been avoided if JavaScript fully embraced its prototypes.

The Many Faces of this

JavaScript inherited the this keyword from Java without also inheriting class syntax until ES6. In classical OOP, this always refers to the current instance. In JavaScript, this can be one of four things depending on how a function is invoked:

  1. Function invocation pattern: Inside a regular function call, this is bound to the global object (or undefined in strict mode).
  2. Method invocation pattern: When a function is referenced as an object property, this binds to the parent object. Arrow functions behave differently — they inherit this from their enclosing scope at creation time rather than having their own binding.
  3. Constructor invocation pattern: With the new prefix, a new empty object is created and this binds to that object.
  4. apply invocation pattern: The apply method inherited from the function prototype accepts a value to bind to this as its first parameter, with an array of function arguments as the second.

Having this mean different things in different contexts arguably makes the language harder to work with than necessary. Workarounds like bind() solve a problem that shouldn't exist. Fortunately, this is entirely avoidable in modern JavaScript if you know how to sidestep it — an advantage ES6 class users don't share.

Crockford captures the frustration well: "This is a demonstrative pronoun. Just having this in the language makes the language harder to talk about. It is like pair programming with Abbott and Costello."

The typical objection is: don't function constructors require this? Not necessarily. You can build a working function constructor without this or new that returns a new object literal directly. The trade-off: objects created this way don't have access to the constructor's prototype, so you can't add methods or properties to SomeConstructor.prototype later. But as we'll see, there are better approaches to code reuse anyway.

The Trouble with new

Crockford's original argument against the new prefix was that nothing guarantees you'll remember to use it on functions that need it. That concern is easier to dismiss today — linters flag capitalized functions called without new, and vice versa. The stronger argument is that new forces you to use this inside constructors and "classes," which we're better off avoiding entirely.

Three Ways to Reference Prototypes

JavaScript offers multiple, confusingly similar ways to work with prototype chains, with little standardization about which to use when:

  • [[Prototype]] — An internal property holding a reference to the object's prototype. The double square brackets signal it normally can't be accessed directly.
  • __proto__ — A deprecated, poorly performing accessor on Object.prototype that exposes the hidden [[Prototype]]. It can also be set as a property in an object literal to link the new object to a specific prototype at creation.
  • .prototype — A property exclusive to functions (not arrow functions). When invoked with new, the instantiated object's prototype points to the function's .prototype.

Prototype Manipulation and Its Costs

The __proto__ literal property can be used at object initialization to bypass constructors entirely. When creating an object literal, you can add __proto__ and the object's prototype will point to the given value. This can link objects created from a custom function constructor back to that constructor's prototype — without using this or new. However, anything meaningful you'd do with that prototype likely requires this inside the constructor methods anyway, which re-introduces the complexity we were trying to avoid. A double method added this way would need access to the encapsulated count value, which isn't accessible from the prototype chain — unless you expose setter methods. At that point, the code is more complicated than simply defining double inside the function itself.

A critical caveat: __proto__ should only be used as a literal property at object creation. Using the accessor Object.prototype.__proto__ to change a prototype after initialization disrupts engine optimizations and is heavily discouraged.

Object.create() returns a new object whose prototype is its first argument, with an optional second argument for defining additional properties. It's more readable to create plain objects with object literals, so Object.create() is mainly useful for creating an object with no prototype at all via Object.create(null). Similarly, Object.setPrototypeOf() mutates an existing object's prototype — but changing prototypes after initialization carries the same performance penalties as the deprecated __proto__ accessor and should be avoided.

Encapsulation Gap in Classes

Class syntax offers no real privacy. All properties are public by default. Closure-based workarounds are possible — moving properties out of this and into the constructor's closure scope — but then properties are inaccessible from methods defined later on the prototype, forcing you to use accessor methods. With multiple arguments, this becomes repetitive:

  • All properties must be re-declared with const inside the constructor.
  • Prototype methods can't access closure-scoped variables without getter methods.
  • Methods themselves remain mutable — users can still overwrite object methods from outside.

Function constructors handle this better. Using closures and Object.freeze(), both data and methods can be made immutable. Attempts to overwrite frozen methods fail silently. Even with the recent proposal for private class fields, one could argue that adding more syntax to the language is unnecessary when custom constructor functions with closures already achieve the same encapsulation.

Composition: A Welcome Alternative

Crockford's later book How JavaScript Works suggests a third path beyond both prototypes and classes: composition. The principle: Instead of inheriting from a base (same as except), assemble objects from small, specialized pieces ("a little bit of this and a little bit of that").

By combining a set of independent function constructors, each handling one responsibility, you can build specialized objects with full privacy and encapsulation — no this, no new, no class hierarchies, no prototype chains to manage. Using prototypes feels like using a half-finished feature, while classes can lead to overcomplicated hierarchies. Fortunately, JavaScript is a multi-paradigm language; limiting yourself to only one approach for code reuse is constraining yourself with imaginary ropes.

The Verdict on JavaScript's OOP

JavaScript's origin story is well known: it was created in ten days, shaped by the commercial pressures of the browser wars, and carries a legacy of questionable design choices. Yet, despite these flaws, it has become a language that powers much of the modern web and continues to drive innovation. That success suggests a resilience and utility that goes beyond its initial compromises.

The same applies to its object-oriented features. The pressure to market JavaScript as a language similar to Java pushed classical inheritance syntax into the language, obscuring its more genuine and powerful prototype-based nature. This has left developers with a split personality: a prototype system that is rarely given the features it deserves and a layer of class syntax that many use out of habit or expectation, rather than necessity.

Choosing to avoid these classical patterns whenever possible is a valid strategy. However, this is a conscious decision that requires a solid understanding of the underlying mechanics. The same applies to many other aspects of the language that have accumulated over the years. Programmers are better off acknowledging the presence of these dubious features and taking the time to learn which parts of the language to actively use and which to intentionally ignore.

There is no expectation that prototypes will suddenly gain the features they deserve or that the industry will stop relying on class-based syntactic sugar. The path forward is not a clean break but a series of informed, sensible choices about which tools to use for the task at hand.

Further Reading

"I don’t think we will see a day when prototypes receive the features they deserve, nor one in which we stop using classical syntactic sugar, but we can decide to avoid them when possible."