Class vs constructor function
A class is syntactic sugar over prototypes, but not only sugar: it adds several hard behavioural rules a constructor function does not have. Both mechanisms eventually create an object with a prototype, yet a class forbids calls without new, turns on strict mode, hides methods from enumeration and is not hoisted up the scope.
Theory
TL;DR
- A class cannot be called without
new: that is aTypeError. A constructor function can be, and it silently pollutes the global object or throws. - A class body always runs in strict mode, even without
'use strict'. - Class methods are non-enumerable, so they never show up in
for...inorObject.keys(Prototype). - Classes are not hoisted: touching a class before its declaration throws a
ReferenceError. - Class inheritance is
extendsplussuper, for functions it is manualObject.createandcall. - Private
#fieldmembers exist only in classes.
Quick example
function UserFn(name) {
this.name = name;
}
UserFn("Alice"); // works, but writes to the global object or throws in strict mode
class UserClass {
constructor(name) { this.name = name; }
}
UserClass("Alice"); // TypeError: Class constructor UserClass cannot be invoked without 'new'Syntax and readability
Constructor function:
function User(name) {
this.name = name;
}
User.prototype.greet = function () {
console.log(`Hi, ${this.name}`);
};
const user = new User("Alice");
user.greet();Class:
class User {
constructor(name) {
this.name = name;
}
greet() {
console.log(`Hi, ${this.name}`);
}
}
const user = new User("Alice");
user.greet();The class looks cleaner and more declarative, especially once inheritance appears. A constructor function forces you to wire up prototype, Object.create() and similar things by hand.
Calling without new, and strict mode
A constructor function can be called without new by accident, and that leads to errors: this becomes undefined in strict mode or binds to the global object.
function User(name) {
this.name = name;
}
const u = User("Alice"); // error, or it creates a global window.name propertyA class cannot be called without new, that is an immediate exception:
class User {
constructor(name) {
this.name = name;
}
}
const u = User("Alice"); // TypeError: Class constructor User cannot be invoked without 'new'This makes classes safer. On top of that, the whole class body automatically runs in strict mode even if 'use strict' is written nowhere. A constructor function gives no such guarantee: without an explicit 'use strict' it runs in sloppy mode.
Enumerability, class fields and hoisting
Methods defined inside class {} are non-enumerable, so they do not appear in a for...in walk, whereas methods on the prototype of a constructor function are enumerable:
class A {
m() {}
}
console.log(Object.keys(A.prototype)); // []
function B() {}
B.prototype.m = function() {};
console.log(Object.keys(B.prototype)); // ["m"]That makes classes "cleaner" for iteration and serialisation.
Inside a class body you could not declare methods as ordinary function expressions:
class A {
m = function() {}; // SyntaxError in older engine versions
}You need the class fields syntax or arrow functions for that, and those were standardised later (ES2022 and newer).
Finally, classes are not hoisted. Constructor functions can be called before they are declared:
const a = new User();
function User() {}Classes do not work that way:
const a = new User(); // ReferenceError
class User {}This prevents surprising runtime errors.
Inheritance
With constructor functions you have to write a lot of boilerplate:
function Animal(name) {
this.name = name;
}
Animal.prototype.speak = function() {
console.log(`${this.name} makes a sound`);
};
function Dog(name) {
Animal.call(this, name);
}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;And with classes:
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a sound`);
}
}
class Dog extends Animal {
speak() {
console.log(`${this.name} barks`);
}
}Short and clear. A class also gives you super for reaching the parent implementation, while with functions you have to call Animal.prototype.speak.call(this) manually.
Summary table
| Difference | Class | Constructor function |
|---|---|---|
Call without new | TypeError | Works, but leads to bugs |
| Strict mode | Always on | Off by default |
| Hoisting | No | Yes |
| Methods enumerable | No | Yes |
| Inheritance syntax | Simple (extends, super) | Complex (Object.create, call) |
| Private fields | Yes (#field) | No |
| Readability | High | Low |
Common mistakes
- Saying in an interview that "a class is just sugar" and stopping there. The behavioural differences (calls without
new, strict mode, hoisting, enumerability) are exactly what is being tested. - Confusing the lack of hoisting with the declaration not being hoisted at all: the class binding is hoisted but sits in the temporal dead zone, which is why you get a
ReferenceErrorand notis not defined. - Forgetting
Dog.prototype.constructor = Dogin manual inheritance and then wondering whyinstance.constructorpoints atAnimal. - Treating
_fieldin a constructor function as private. Real privacy exists only in class fields with#. - Passing a class method as a callback and losing
this: neither a class nor a constructor function binds the context for you.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.