What are ES2015 classes and how do they differ from ES5 function constructors?
The short answer
ES2015 classes are syntactic sugar over JavaScript's existing prototype-based inheritance. Under the hood, they do the same thing as ES5 constructor functions and prototype assignments — they just provide a cleaner, more familiar syntax for creating objects and setting up inheritance. If you understand prototypes, classes are just a nicer way to write them.
The ES5 way: constructor functions and prototypes
Before classes existed, you created "blueprints" for objects using regular functions and the new keyword:
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.greet = function () {
return `Hi, I'm ${this.name} and I'm ${this.age} years old.`;
};
const alice = new Person('Alice', 30);
console.log(alice.greet()); // "Hi, I'm Alice and I'm 30 years old."Here's what happens when you call new Person('Alice', 30): a new empty object is created, its internal prototype is set to Person.prototype, the function runs with this pointing to the new object, and the new object is returned. Methods go on the prototype so they're shared across all instances rather than being duplicated on each object.
The ES2015 way: class syntax
The same thing written with a class:
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
return `Hi, I'm ${this.name} and I'm ${this.age} years old.`;
}
}Cleaner to read, and the intent is immediately clear. But under the hood, Person is still a function, and greet still lives on Person.prototype:
console.log(typeof Person); // 'function'
console.log(alice.__proto__ === Person.prototype); // trueKey differences that matter
Even though classes compile down to the same prototype-based system, there are real behavioral differences:
Classes are not hoisted. Function declarations are hoisted — you can call them before they appear in your code. Classes exist in the temporal dead zone (like let and const), so using one before its declaration throws a ReferenceError.
Classes enforce new. You can call a regular function without new, which leads to bugs where this points to the global object. Classes throw a TypeError if you forget new.
function Dog(name) {
this.name = name;
}
const d = Dog('Rex'); // oops — no error, but this.name goes to window
class Cat {
constructor(name) {
this.name = name;
}
}
const c = Cat('Whiskers'); // TypeError: cannot be invoked without 'new'Classes run in strict mode automatically. With ES5 constructors, you had to add 'use strict' manually.
Class methods are non-enumerable. Methods defined inside a class don't show up in for...in loops or Object.keys(), which is usually the behavior you want. Prototype methods added the ES5 way are enumerable by default.
Inheritance: extends and super
This is where classes really shine in readability. Here's inheritance the ES5 way:
function Animal(name) {
this.name = name;
}
Animal.prototype.speak = function () {
return `${this.name} makes a sound.`;
};
function Dog(name, breed) {
Animal.call(this, name);
this.breed = breed;
}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;That Object.create / constructor dance is error-prone and hard to read. With classes:
class Animal {
constructor(name) {
this.name = name;
}
speak() {
return `${this.name} makes a sound.`;
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name);
this.breed = breed;
}
fetch() {
return `${this.name} fetches the ball!`;
}
}
const rex = new Dog('Rex', 'Labrador');
console.log(rex.speak()); // 'Rex makes a sound.'
console.log(rex.fetch()); // 'Rex fetches the ball!'extends sets up the prototype chain, and super() calls the parent constructor. The intent is immediately clear.
Static methods
Both approaches support static methods — methods that belong to the constructor itself rather than instances:
// ES5
MathHelper.add = function (a, b) {
return a + b;
};
// ES2015
class MathHelper {
static add(a, b) {
return a + b;
}
}Static methods are called on the class, not on instances. They're useful for utility functions that relate to the class but don't need access to instance data.
When you still see constructor functions
Constructor functions aren't going away. You'll encounter them in older codebases, libraries written before ES2015, and in code that leverages the more flexible nature of functions — like adding properties conditionally or using closures for private state. Understanding both patterns means you can work with any JavaScript codebase, regardless of when it was written.
Why interviewers ask this
This question tests whether you understand that JavaScript's "classes" are not classes in the traditional OOP sense — they're a layer of syntax over prototypal inheritance. Interviewers want to see that you know what's happening beneath the class keyword, can explain the prototype chain, and understand the practical differences. It separates developers who truly understand JavaScript's object model from those who just memorized the class syntax from another language.