JavaScript Classes and Prototypal Inheritance

JavaScript classes, introduced in ECMAScript 2015 (ES6), provide a cleaner and more familiar syntax for object-oriented programming, but they do not introduce a new object-oriented inheritance model. Instead, the class syntax acts as “syntactic sugar” over JavaScript’s existing prototypal inheritance mechanism. Under the hood, objects still inherit properties and methods directly from other objects via prototype chains.

Classes Are Functions

At runtime, a JavaScript class is fundamentally a constructor function. When you declare a class, JavaScript creates a function with the same name.

class Person {
  constructor(name) {
    this.name = name;
  }
}

console.log(typeof Person); // "function"

The constructor method defined inside the class serves as the actual function body executed when using the new keyword.

Method Placement on the Prototype

In classical prototypal JavaScript, shared methods are manually assigned to a function’s prototype object to ensure instances share a single copy in memory:

function Person(name) {
  this.name = name;
}

Person.prototype.greet = function() {
  return `Hello, my name is ${this.name}`;
};

When using class syntax, any method declared inside the class body (outside the constructor) is automatically assigned to Person.prototype:

class Person {
  constructor(name) {
    this.name = name;
  }

  greet() {
    return `Hello, my name is ${this.name}`;
  }
}

console.log(Person.prototype.hasOwnProperty('greet')); // true

When an instance calls greet(), the JavaScript engine checks the instance itself first. If it cannot find it, it traverses up the internal prototype link ([[Prototype]] or __proto__) to Person.prototype.

Inheritance with extends and super

The extends keyword establishes a prototype chain between two constructor functions and their prototypes.

Consider this subclass:

class Employee extends Person {
  constructor(name, title) {
    super(name);
    this.title = title;
  }

  work() {
    return `${this.name} is working as a ${this.title}`;
  }
}

Behind the scenes, extends configures two separate prototype links:

  1. Instance Prototype Chain: It links Employee.prototype.[[Prototype]] to Person.prototype. This allows Employee instances to access methods defined on Person.prototype.
  2. Static Prototype Chain: It links Employee.[[Prototype]] to Person. This allows static methods defined on Person to be inherited by Employee.

The super(name) call invokes the parent constructor (Person), passing the context of the newly created Employee instance so properties like this.name can be initialized.

Key Differences from Pure Constructor Functions

While the class syntax translates to prototypal patterns, the JavaScript engine enforces a few stricter rules:

The class syntax simplifies the creation and maintenance of prototype chains, offering a more declarative structure while relying entirely on JavaScript’s prototype-based object model.