JavaScript classes and prototypes are not two competing systems. They are one system wearing two outfits: the class keyword builds a constructor function and gives it a prototype object, and every property lookup in the language still ends with a walk up that prototype chain. Once you can see that arrangement, the parts of the language that look like magic stop looking like magic.
This page takes the machinery apart. You will build the same shape twice — once with a plain constructor function, once with class syntax — and then poke at the result with Object.getPrototypeOf, Object.keys and hasOwnProperty until the relationship between the three objects involved (the constructor, its prototype, and the instance) is obvious rather than assumed.
What does the class keyword actually build?
Start with the older form: a function that assigns properties to this, plus methods bolted onto Rectangle.prototype. This is not a recommendation to write new code this way. It is the clearest way to see what the class form is doing underneath.
function Rectangle(width, height) {
this.width = width;
this.height = height;
}
Rectangle.prototype.area = function () {
return this.width * this.height;
};
const box = new Rectangle(3, 4);
console.log(box.area());
console.log(box instanceof Rectangle);
console.log(Object.getPrototypeOf(box) === Rectangle.prototype);
Three objects are in play here and they are easy to confuse. Rectangle is a function. Rectangle.prototype is an ordinary object that holds the shared behaviour. box is an ordinary object whose internal prototype link points at Rectangle.prototype. The new keyword runs the function with this bound to a freshly created object, points that object's prototype at Rectangle.prototype, and hands the object back.
Now the class form, which produces the same arrangement with less ceremony.
class Rectangle {
constructor(width, height) {
this.width = width;
this.height = height;
}
area() {
return this.width * this.height;
}
}
const box = new Rectangle(3, 4);
console.log(box.area());
console.log(typeof Rectangle);
console.log(Object.getPrototypeOf(box) === Rectangle.prototype);
The two snippets print the same area and the same true, and the reason is that class is a more disciplined way of spelling the previous example. Notice what typeof Rectangle reports: function, not class. There is no separate class object in the runtime. The methods you declared inside the class body were placed on Rectangle.prototype for you, and constructor became the function you call with new.
Where do the methods actually live?
They live on the prototype, not on the instance. That is not trivia, because it decides whether a method is shared by every object of that type or copied onto each one. Most languages copy; JavaScript shares and looks up.
class Rectangle {
constructor(width, height) {
this.width = width;
this.height = height;
}
area() {
return this.width * this.height;
}
}
const box = new Rectangle(3, 4);
console.log(Object.keys(box));
console.log(box.hasOwnProperty("area"));
console.log(box.area === Rectangle.prototype.area);
console.log(Object.getOwnPropertyNames(Rectangle.prototype));
Object.keys(box) lists only width and height — the per-object state. The method is not among them, and hasOwnProperty("area") answers false. The method is found by walking the prototype chain, and box.area === Rectangle.prototype.area answers true, which means every rectangle points at one and the same function object. Build a million rectangles and you have created one area function, not a million. That single fact is why prototype-based languages can afford to add a method to a type after objects of that type already exist.
What is the prototype chain, exactly?
Every object has exactly one internal link to another object, and Object.getPrototypeOf is how you look at it from user code. Reading a property means: look on the object itself; if it is not there, look on its prototype; if it is not there, look on that object's prototype, and keep going until you reach null.
class Rectangle {
constructor(width, height) {
this.width = width;
this.height = height;
}
area() {
return this.width * this.height;
}
}
const box = new Rectangle(3, 4);
const proto = Object.getPrototypeOf(box);
console.log(proto === Rectangle.prototype);
console.log(proto.hasOwnProperty("area"));
console.log(Object.getPrototypeOf(proto) === Object.prototype);
console.log(typeof box.toString);
console.log(Object.getPrototypeOf(Object.getPrototypeOf(proto)) === null);
The chain for box runs box to Rectangle.prototype to Object.prototype to null. That last stop explains why every object inherits toString and hasOwnProperty without anyone defining them, and why asking for the prototype of Object.prototype gives null rather than an error.
How does class syntax compare with a constructor function?
Both styles produce the same runtime shape. The differences are about safety and ergonomics, not about capability.
| Behaviour | Constructor function | class syntax |
|---|---|---|
| Prototype methods | Assigned by hand onto Foo.prototype | Declared in the class body |
Called without new | Runs, with this undefined or global | Throws a TypeError |
Prototype constructor property | Points at the function, but can be clobbered by accident | Non-enumerable and always correct |
| Method enumerability | Enumerable by default when assigned by hand | Non-enumerable |
| Reaching the parent implementation | Manual prototype juggling | super(...) and super.method() |
The second row bites people most often. A class cannot be invoked without new:
class Rectangle {
constructor(width, height) {
this.width = width;
this.height = height;
}
}
try {
Rectangle(3, 4);
} catch (err) {
console.log(err.constructor.name + ": " + err.message);
}
The message names the fix: a constructor needs new. The fourth row matters just as much in practice. Methods declared in a class body are non-enumerable, so for...in and Object.keys skip them, whereas a method you bolted onto a prototype by hand is enumerable and shows up in both. Move a codebase from one style to the other and any code that loops over an object with for...in will quietly start behaving differently.
Can an object inherit without any class being involved?
Yes, and it is worth understanding because it is what class compiles down to anyway. Object.create takes a prototype object and returns a new object whose internal link points at it.
const greeter = {
greet() {
return this.name + " says hello";
},
};
const robot = Object.create(greeter);
robot.name = "R2";
console.log(robot.greet());
console.log(robot.hasOwnProperty("greet"));
console.log(Object.getPrototypeOf(robot) === greeter);
console.log(Object.getPrototypeOf(robot).hasOwnProperty("greet"));
The first line prints R2 says hello even though robot was created from an object literal, because greet was found by delegation. The second line prints false: the robot never owned that function. The last two lines print true, confirming that the robot's prototype really is the greeter object and that greet lives on it. Sharing behaviour without copying it is the whole trick, and class syntax is a tidy front end for the same idea.
Quick reference: the model in five lines
- There is no separate class object in the runtime.
classproduces a constructor function plus a prototype object. - Behaviour lives on the prototype and is shared by every instance. State lives on the instance and is per object.
- A read walks the chain: instance, then its prototype, then that prototype's prototype, then
null. Object.getPrototypeOfshows the link,hasOwnPropertyshows what is genuinely yours, andObject.keysshows your own enumerable data.- Methods can be added to a prototype after instances exist, because nothing was copied.
Try it yourself: in a Node REPL or a browser console, declare class Point { constructor(x, y) { this.x = x; this.y = y; } distanceTo(other) { return Math.hypot(this.x - other.x, this.y - other.y); } }, then create two points. Check Object.keys(p1), p1.hasOwnProperty("distanceTo"), Object.getPrototypeOf(p1) === Point.prototype and Object.getPrototypeOf(Point.prototype) === Object.prototype — the last two should print true. Then, after both points exist, add Point.prototype.toString = function () { return this.x + "," + this.y; } and call it on a point you created earlier. It works, because behaviour is looked up rather than copied.