What's the Difference Between JavaScript Classes and Prototypes?
Problem
I saw a Reddit discussion where someone asked: “What’s the use of classes in JS?” The top answer was blunt: “JS classes are just syntactical sugar around prototypes.”
That answer confused me more than it helped. If classes are just sugar, why do they exist? Are they different from prototypes or not? And what does “syntactical sugar” even mean in practice?
I decided to dig in and find out what’s actually happening under the hood.
The Confusion
When ES6 introduced classes in 2015, many developers thought JavaScript had finally “fixed” its inheritance model. Coming from Java or C++, the class keyword felt familiar:
class Person { constructor(name) { this.name = name; } greet() { return `Hello, I'm ${this.name}`; }}But then I learned that this is “just syntactical sugar” over prototypes. What does that mean?
Let me show you the equivalent code before ES6:
function Person(name) { this.name = name;}Person.prototype.greet = function() { return `Hello, I'm ${this.name}`;};Both pieces of code do the same thing. The class version is cleaner and more familiar to developers from other languages, but under the hood, JavaScript is still using prototypal inheritance.
Proof: Classes Are Functions
The first thing I tested was typeof:
class Person { constructor(name) { this.name = name; }}
console.log(typeof Person); // 'function'A class IS a function. JavaScript didn’t introduce a new type. It just created a cleaner syntax for creating constructor functions and setting up prototype chains.
What Actually Changes
If classes are just sugar, why use them? I found several behavioral differences that matter in practice.
Difference 1: Strict Mode
Classes always run in strict mode, whether you declare it or not:
// Constructor function - strict mode is optionalfunction User(name) { // this works without strict mode undeclaredVariable = name; // Creates global variable (bad!)}
// Class - strict mode is ALWAYS enforcedclass UserClass { constructor(name) { undeclaredVariable = name; // ReferenceError: undeclaredVariable is not defined }}This prevents common mistakes like accidentally creating global variables.
Difference 2: Calling Without new
Constructor functions let you call them without new, which creates subtle bugs:
// Constructor function - no error when called without newfunction User(name) { this.name = name;}const broken = User('Alice'); // No error!console.log(broken); // undefinedconsole.log(window.name); // 'Alice' - polluted global scope!
// Class - enforces the 'new' keywordclass UserClass { constructor(name) { this.name = name; }}const works = UserClass('Bob'); // TypeError: Class constructor UserClass cannot be invoked without 'new'Classes protect you from this common mistake. Constructor functions silently fail in confusing ways.
Difference 3: Hoisting Behavior
Function declarations are hoisted completely. Classes are not:
// Function declaration - hoistedconst person1 = new Person('Alice'); // Works!
function Person(name) { this.name = name;}
// Class - NOT hoisted the same wayconst person2 = new PersonClass('Bob'); // ReferenceError: Cannot access 'PersonClass' before initialization
class PersonClass { constructor(name) { this.name = name; }}Classes are hoisted but exist in a “temporal dead zone” like let and const. This prevents you from using them before they’re defined.
Difference 4: Method Enumerability
Methods defined in a class are non-enumerable by default:
// Constructor function - methods are enumerablefunction Person(name) { this.name = name;}Person.prototype.greet = function() { return `Hello, ${this.name}`;};
const p1 = new Person('Alice');console.log(Object.keys(p1)); // ['name']console.log(Object.keys(Person.prototype)); // ['greet'] - enumerable!
// Class - methods are non-enumerableclass PersonClass { constructor(name) { this.name = name; } greet() { return `Hello, ${this.name}`; }}
const p2 = new PersonClass('Bob');console.log(Object.keys(p2)); // ['name']console.log(Object.keys(PersonClass.prototype)); // [] - non-enumerable!This matters when you iterate over object properties. Class methods won’t show up in for...in loops by default.
Comparison Table
| Feature | Class | Constructor Function |
|---|---|---|
| Strict mode | Always enforced | Optional |
| Hoisting | Temporal dead zone | Fully hoisted (declarations) |
Calling without new | TypeError | Works (but this is wrong) |
| Enumerable methods | Non-enumerable by default | Enumerable if defined normally |
typeof result | 'function' | 'function' |
| Uses prototypes | Yes | Yes |
Inheritance: Class vs Prototype Chain
The most compelling reason to use classes is inheritance syntax. Here’s the comparison:
Class Inheritance (Clean)
class Animal { constructor(name) { this.name = name; } speak() { return `${this.name} makes a sound`; }}
class Dog extends Animal { constructor(name, breed) { super(name); // Call parent constructor this.breed = breed; } speak() { return `${this.name} barks!`; }}
const dog = new Dog('Max', 'Labrador');console.log(dog.speak()); // "Max barks!"Prototype Inheritance (Verbose)
function Animal(name) { this.name = name;}Animal.prototype.speak = function() { return `${this.name} makes a sound`;};
function Dog(name, breed) { Animal.call(this, name); // Call parent constructor this.breed = breed;}// Set up prototype chainDog.prototype = Object.create(Animal.prototype);Dog.prototype.constructor = Dog; // Fix constructor referenceDog.prototype.speak = function() { return `${this.name} barks!`;};
const dog = new Dog('Max', 'Labrador');console.log(dog.speak()); // "Max barks!"The prototype version requires more boilerplate and is easier to get wrong:
- Forget
Object.create()? Inheritance breaks. - Forget to fix
constructor?dog.constructorpoints to the wrong function. - Forget
Animal.call()? Parent properties aren’t set.
Common Mistakes
Mistake 1: Treating JS Classes Like Java Classes
I used to think JavaScript classes worked like Java classes. They don’t.
// In Java, this would create a private instance variableclass Counter { count = 0; // This is a public field in JS increment() { this.count++; }}
const c = new Counter();c.count = 100; // No privacy! Anyone can access and modifyJavaScript classes don’t provide true encapsulation by default. Private fields (using #) are a recent addition:
class Counter { #count = 0; // Truly private increment() { this.#count++; } getCount() { return this.#count; }}
const c = new Counter();c.increment();console.log(c.getCount()); // 1console.log(c.#count); // SyntaxError: Private field '#count' must be declared in an enclosing classMistake 2: Ignoring What’s Under the Hood
Because classes hide the prototype mechanics, it’s easy to forget they exist:
class Person { constructor(name) { this.name = name; }}
// You can still modify the prototype!Person.prototype.farewell = function() { return `Goodbye from ${this.name}`;};
const person = new Person('Alice');console.log(person.farewell()); // "Goodbye from Alice"The prototype chain is still there. Classes just make it less visible.
Mistake 3: Hoisting Confusion
I expected classes to work like function declarations:
// This works with function declarationsrun(); // "Running!"
function run() { console.log("Running!");}
// This fails with classesconst p = new Person(); // ReferenceError!
class Person { constructor() { console.log("Created!"); }}Classes behave like let declarations: they exist in the temporal dead zone until the line where they’re defined.
When to Use Classes vs Constructor Functions
Use classes when:
- You want cleaner syntax
- You need inheritance (
extends,super) - You want automatic strict mode enforcement
- You’re working on a team where OOP syntax is more familiar
Stick with constructor functions when:
- You’re working with older codebases
- You need enumerable methods (rare)
- You specifically need hoisting behavior (even rarer)
In modern JavaScript, classes are the default choice. The syntax is cleaner, the behavior is more predictable, and the common pitfalls are guarded against.
Summary
JavaScript classes are syntactical sugar, but that doesn’t mean they’re pointless. They:
- Enforce strict mode automatically
- Throw errors when called without
new - Make methods non-enumerable by default
- Provide cleaner inheritance syntax with
extendsandsuper - Prevent hoisting-related bugs
Under the hood, they still use prototypes. But the sugar makes the code safer and more readable.
The key insight from the Reddit discussion was correct: classes don’t change how JavaScript inheritance works. They just make it harder to shoot yourself in the foot.
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
- 👨💻 MDN: Classes
- 👨💻 MDN: Inheritance and the prototype chain
- 👨💻 Reddit: What's the use of classes in JS
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments