ES6 Classes

intermediate class constructor static syntactic-sugar

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.

References