#Classes in JavaScript
A class is syntactic sugar over constructor functions and prototypes — the prototype chain underneath is exactly the same. But classes add a few real rules: they must be called with new, their bodies are always strict mode, and they're in the TDZ until declared (not hoisted like function declarations).
Before ES2015 you built the same thing by hand: write a constructor function, then bolt methods onto Constructor.prototype one assignment at a time, then wire up inheritance with Object.create and a call to the parent constructor (see What new actually does and Inheritance between constructor functions). It worked, but it was verbose and easy to get subtly wrong. class gives you one declaration that sets all of that up correctly.
The mental model: a class declaration creates two objects. The class itself is a function (the constructor) — it holds static members. Attached to it is Person.prototype, an ordinary object that holds the methods. Every instance is a thin object holding only its own data, with a hidden link to that prototype. Methods are looked up through the link, so a thousand instances share one copy of each method (see The prototype chain).
class Person {
// Public instance field — one per instance
species = "human";
constructor(name, age) {
this.name = name;
this.age = age;
}
// Method — goes on Person.prototype, shared by all instances
greet() {
return `Hello, my name is ${this.name} and I'm ${this.age}.`;
}
// Static — lives on the class itself
static compare(a, b) {
return a.age - b.age;
}
}
const john = new Person("John", 30);
john.greet();
typeof Person; // "function"
Object.getPrototypeOf(john) === Person.prototype; // true
Person(); // TypeError: Class constructor Person cannot be invoked without 'new'- Evaluating the
classdeclaration creates the functionPerson(its body is theconstructor), putsgreetonPerson.prototype, and putscomparedirectly onPerson.Object.getOwnPropertyNames(Person.prototype)is["constructor", "greet"]. Thespeciesfield is not stored anywhere yet — it's remembered as an instruction to run for every new instance. new Person("John", 30)creates an empty object whose prototype isPerson.prototype. Field initialisers run first, so it getsspecies: "human". Then the constructor body runs withthisset to that object:namebecomes"John",agebecomes30.Object.keys(john)is["species", "name", "age"]— only data, no methods.john.greet()—johnhas no owngreet, so the engine follows the link toPerson.prototype, finds it there, and calls it withthis = john→"Hello, my name is John and I'm 30.".typeof Personis"function"andjohn's prototype isPerson.prototype— proof that a class is a constructor function plus a prototype object, nothing more exotic.comparelives on the class, so you call it asPerson.compare(a, b)— for examplepeople.sort(Person.compare)sorts youngest first.john.compareisundefined: statics are not on the prototype, so instances can't see them.Person()withoutnewthrows. A pre-ES2015 constructor function called withoutnewwould just run with the wrongthis(the global object, orundefinedin strict mode) and fail silently or confusingly; classes refuse up front.
The concrete differences from a hand-written constructor function:
- Must be called with
new— otherwiseTypeError(step 6 above). - Always strict mode inside the class body, so a detached method gets
this === undefinedinstead of the global object. - TDZ, not hoisting —
new X()beforeclass X {}throwsReferenceError: Cannot access 'X' before initialization. A function declaration could be used before its line. - Methods are non-enumerable.
for...inover an instance doesn't listgreet. WithPerson.prototype.greet = function () {}, it would. - Fields,
#privatemembers, static blocks andsuperhave no clean equivalent in constructor-function code.
When to reach for a class: when you'll create many objects with the same shape and behaviour (models, UI components, errors), when a framework expects one (Angular services and components, web components extending HTMLElement, custom Error types), or when you want real private state with #. When you only need one object or a bag of related functions, a plain object or a module is simpler — there's nothing to instantiate.