ES6 classes are one of those features that make JavaScript feel more like a “traditional” OOP language. But here’s the thing — classes in JavaScript are just syntactic sugar over the prototype system. Under the hood, they work exactly like constructor functions and prototypes. The class keyword just gives us a cleaner, more familiar syntax.
Basic class syntax
class Person {
constructor(name, age) {
this.name = name; // instance property
this.age = age;
}
greet() { // instance method (goes on prototype)
return `Hi, I'm ${this.name}`;
}
}
const manish = new Person("Manish", 25);
console.log(manish.greet()); // "Hi, I'm Manish"
The constructor method runs automatically when we call new Person(...). Methods we define inside the class body (like greet) go on Person.prototype, not on each instance — so they’re shared, just like with constructor functions.
Classes are just functions
This is worth proving to ourselves:
console.log(typeof Person); // "function"
console.log(Person.prototype.greet); // [Function: greet]
A class is literally a function. The constructor becomes the function body, and class methods become prototype methods. So class Person { ... } is essentially doing the same thing as defining a constructor function and attaching methods to its prototype.
Static methods and properties
Static members belong to the class itself, not to instances. We use the static keyword:
class MathHelper {
static PI = 3.14159;
static square(x) {
return x * x;
}
}
console.log(MathHelper.PI); // 3.14159
console.log(MathHelper.square(5)); // 25
// const m = new MathHelper();
// m.square(5); // TypeError — static methods aren't on instances
Think of static methods like utility functions that are related to the class but don’t need an instance. Array.isArray(), Object.keys(), Number.isNaN() — all static methods.
Class expressions
Just like functions, classes can be expressions too:
const Animal = class {
constructor(type) {
this.type = type;
}
};
// Named class expression
const Vehicle = class Car {
// "Car" is only accessible inside this class body
};
Class expressions are less common, but they come in handy when we need to dynamically create classes or pass them around.
Class hoisting (TDZ)
This is a key difference from function declarations. Classes are NOT hoisted like functions. They sit in the Temporal Dead Zone (TDZ) just like let and const:
// const p = new Person(); // ReferenceError!
class Person {
constructor() {
this.name = "Manish";
}
}
const p = new Person(); // works here
With function declarations, we can call them before the line they’re declared on. With classes, we can’t. We must define the class before using it.
Must use new
Unlike constructor functions, classes cannot be called without new. JavaScript throws an error:
// Person("Manish"); // TypeError: Class constructor Person cannot be invoked without 'new'
const p = new Person("Manish"); // correct
This is actually a nice safety feature — remember how forgetting new with constructor functions silently messes up this? Classes prevent that.
Class fields (public instance fields)
We can declare properties directly in the class body without putting them in the constructor:
class Counter {
count = 0; // public field — each instance gets its own copy
increment() {
this.count++;
}
}
const c = new Counter();
c.increment();
console.log(c.count); // 1
Class fields are defined on the instance, not on the prototype. They run at the beginning of the constructor (or when super() returns in subclasses).
Methods are non-enumerable
One subtle difference from manually attaching methods to a prototype — class methods are non-enumerable. This means they won’t show up in for...in loops or Object.keys():
class Dog {
bark() { return "Woof!"; }
}
const d = new Dog();
console.log(Object.keys(d)); // [] — bark is not enumerable
This is actually the correct behavior. We usually don’t want methods to show up when we iterate over an object’s properties.
Putting it all together
Here’s a practical example that uses most of what we’ve covered:
class User {
static count = 0; // static field
constructor(name, role) {
this.name = name;
this.role = role;
User.count++; // track how many users were created
}
describe() {
return `${this.name} (${this.role})`;
}
static totalUsers() {
return `Total users: ${User.count}`;
}
}
const u1 = new User("Manish", "admin");
const u2 = new User("Priya", "editor");
console.log(u1.describe()); // "Manish (admin)"
console.log(User.totalUsers()); // "Total users: 2"
In simple language, ES6 classes are a nicer way to write constructor functions and prototype methods. They don’t introduce a new inheritance model — they just make the existing prototype system easier to read and write. Under the hood, typeof MyClass is still "function", and methods still live on the prototype.