Skip to content

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:

familiar_syntax.js
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:

prototype_version.js
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:

type_test.js
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:

strict_mode.js
// Constructor function - strict mode is optional
function User(name) {
// this works without strict mode
undeclaredVariable = name; // Creates global variable (bad!)
}
// Class - strict mode is ALWAYS enforced
class 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:

new_keyword.js
// Constructor function - no error when called without new
function User(name) {
this.name = name;
}
const broken = User('Alice'); // No error!
console.log(broken); // undefined
console.log(window.name); // 'Alice' - polluted global scope!
// Class - enforces the 'new' keyword
class 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:

hoisting.js
// Function declaration - hoisted
const person1 = new Person('Alice'); // Works!
function Person(name) {
this.name = name;
}
// Class - NOT hoisted the same way
const 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:

enumerable.js
// Constructor function - methods are enumerable
function 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-enumerable
class 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

FeatureClassConstructor Function
Strict modeAlways enforcedOptional
HoistingTemporal dead zoneFully hoisted (declarations)
Calling without newTypeErrorWorks (but this is wrong)
Enumerable methodsNon-enumerable by defaultEnumerable if defined normally
typeof result'function''function'
Uses prototypesYesYes

Inheritance: Class vs Prototype Chain

The most compelling reason to use classes is inheritance syntax. Here’s the comparison:

Class Inheritance (Clean)

class_inheritance.js
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)

prototype_inheritance.js
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 chain
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog; // Fix constructor reference
Dog.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.constructor points 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.

java_vs_js.js
// In Java, this would create a private instance variable
class 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 modify

JavaScript classes don’t provide true encapsulation by default. Private fields (using #) are a recent addition:

private_fields.js
class Counter {
#count = 0; // Truly private
increment() {
this.#count++;
}
getCount() {
return this.#count;
}
}
const c = new Counter();
c.increment();
console.log(c.getCount()); // 1
console.log(c.#count); // SyntaxError: Private field '#count' must be declared in an enclosing class

Mistake 2: Ignoring What’s Under the Hood

Because classes hide the prototype mechanics, it’s easy to forget they exist:

prototype_still_exists.js
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:

hoisting_bug.js
// This works with function declarations
run(); // "Running!"
function run() {
console.log("Running!");
}
// This fails with classes
const 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:

  1. Enforce strict mode automatically
  2. Throw errors when called without new
  3. Make methods non-enumerable by default
  4. Provide cleaner inheritance syntax with extends and super
  5. 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:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments