Skip to main content

Classes and prototypes

Classes in JavaScript are a convenient wrapper over constructor functions and prototypes: the language underneath has not changed, only the syntax has. A class creates a constructor function, puts the methods on its prototype and, when extends is present, builds a prototype chain.

Theory

TL;DR

  • A class is a function: typeof User === 'function'.
  • Class methods are written to User.prototype, not into every instance.
  • new User() creates an object, sets User.prototype as its prototype and runs the constructor.
  • extends links Child.prototype to Parent.prototype, and Child to Parent for static members.
  • Calling a method is a lookup up the prototype chain; instanceof checks that same chain.
  • The sugar is not fully transparent: class code is strict, its methods are non enumerable, and it cannot be called without new.

Quick example

javascript
class User { constructor(name) { this.name = name; } sayHi() { console.log(`Hello, ${this.name}!`); } } // Roughly the equivalent before class existed: function UserOld(name) { this.name = name; } UserOld.prototype.sayHi = function () { console.log(`Hello, ${this.name}!`); };

Both variants produce the same structure: data on the instance, methods on the prototype.

What happens on new

When you write const user = new User('Maria'), the engine performs four steps:

  1. Creates a new empty object.
  2. Sets its internal [[Prototype]] to User.prototype.
  3. Runs the constructor with this bound to that object.
  4. Returns the object, unless the constructor explicitly returns another object.
javascript
const user = new User('Maria'); console.log(Object.getPrototypeOf(user) === User.prototype); // true console.log(user.constructor === User); // true console.log(Object.keys(user)); // ['name'], data only

The instance stores only its own data and takes behaviour from the prototype.

Methods are properties of prototype

Every method declared in a class body automatically goes to Class.prototype, so all instances share one function in memory.

javascript
console.log(Object.getOwnPropertyNames(User.prototype)); // ['constructor', 'sayHi'] const u1 = new User('Maria'); const u2 = new User('Oleh'); console.log(u1.sayHi === u2.sayHi); // true, one method for everybody

This is the core idea of prototypes: do not duplicate functions in every object. The exceptions are class fields (name = 'default') and methods declared as arrow function properties: those are created per instance.

extends is prototypal inheritance

extends does nothing new, it only links prototypes.

javascript
class Animal { speak() { console.log('The animal makes a sound'); } } class Dog extends Animal { bark() { console.log('Woof woof!'); } } const d = new Dog(); d.speak(); // 'The animal makes a sound'

Under the hood this is roughly what happens:

javascript
// links the instance prototypes: methods Object.setPrototypeOf(Dog.prototype, Animal.prototype); // links the constructors themselves: static members Object.setPrototypeOf(Dog, Animal);

Hence two parallel chains. For instances:

d -> Dog.prototype -> Animal.prototype -> Object.prototype -> null

For constructors:

Dog -> Animal -> Function.prototype -> Object.prototype -> null

You can confirm all of it in code:

javascript
console.log(Object.getPrototypeOf(d) === Dog.prototype); // true console.log(Object.getPrototypeOf(Dog.prototype) === Animal.prototype); // true console.log(Object.getPrototypeOf(Animal.prototype) === Object.prototype); // true console.log(Object.getPrototypeOf(Dog) === Animal); // true

The second chain explains why parent static methods are reachable through the child class: Dog.someStaticMethod() is found on Animal.

How a method is looked up and what instanceof checks

A d.speak() call runs like this:

  1. Does d itself have an own speak property? No.
  2. The engine looks in Dog.prototype. No.
  3. Then in Animal.prototype. Found, called with this === d.
  4. If it were still missing, the search would continue to Object.prototype and finally yield undefined.

instanceof walks the same chain: it checks whether Constructor.prototype appears among the object prototypes.

javascript
console.log(d instanceof Dog); // true console.log(d instanceof Animal); // true, Animal.prototype is in the chain console.log(d instanceof Object); // true

The super keyword also works through prototypes: super.speak() calls the method found on Object.getPrototypeOf(Dog.prototype) with the current this, while super(...) in a constructor invokes the parent constructor, which is what creates this for a derived class.

What a class does under the hood, and how it still differs

The equivalent without class looks like this:

javascript
function Animal() {} Animal.prototype.move = function () {}; function Dog() { Animal.call(this); } Dog.prototype = Object.create(Animal.prototype); Dog.prototype.constructor = Dog; Dog.prototype.bark = function () {}; const dog = new Dog(); dog.bark();
Class constructWhat happens under the hood
constructorthe body of the constructor function
class methoda property on Class.prototype
static methoda property on the Class function itself
class fieldan own instance property created in the constructor
extendsChild.prototype points to Parent.prototype, Child to Parent
super()a call to the parent constructor, which creates this
instanceofa prototype chain check

The sugar is not entirely transparent though, and interviewers ask about these differences:

  • Class code always runs in strict mode.
  • Class methods are non enumerable, so they never show up in for...in.
  • A class cannot be called without new: doing so throws a TypeError.
  • A class declaration is not hoisted the way a function is: before its line it sits in the TDZ.
  • In a derived class this is unavailable until super() has been called.

Common mistakes

  • Treating a class as a separate OOP model. JavaScript has no classes in the Java or C# sense: there are only objects and prototype links.
  • Confusing User.prototype with Object.getPrototypeOf(User). The first is the prototype of future instances, the second is the prototype of the class function itself, which is where static members come from.
  • Thinking methods are copied into the instance. They live on one shared prototype, which is why u1.sayHi === u2.sayHi.
  • Calling a class without new. An old style constructor function tolerates it (and corrupts this), while a class throws a TypeError.
  • Assigning arrow functions to fields without a reason. handle = () => {} creates a new function per instance and never reaches the prototype, so memory grows.
  • Forgetting super() in a derived constructor. Without it, touching this throws a ReferenceError, because the object does not exist yet.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.