Classes and prototypes
Classes in JavaScript are a convenient wrapper over constructor functions and prototypes: the language underneath has not changed, only the syntax has. A class creates a constructor function, puts the methods on its prototype and, when extends is present, builds a prototype chain.
Theory
TL;DR
- A
classis a function:typeof User === 'function'. - Class methods are written to
User.prototype, not into every instance. new User()creates an object, setsUser.prototypeas its prototype and runs the constructor.extendslinksChild.prototypetoParent.prototype, andChildtoParentfor static members.- Calling a method is a lookup up the prototype chain;
instanceofchecks that same chain. - The sugar is not fully transparent: class code is strict, its methods are non enumerable, and it cannot be called without
new.
Quick example
class User {
constructor(name) {
this.name = name;
}
sayHi() {
console.log(`Hello, ${this.name}!`);
}
}
// Roughly the equivalent before class existed:
function UserOld(name) {
this.name = name;
}
UserOld.prototype.sayHi = function () {
console.log(`Hello, ${this.name}!`);
};Both variants produce the same structure: data on the instance, methods on the prototype.
What happens on new
When you write const user = new User('Maria'), the engine performs four steps:
- Creates a new empty object.
- Sets its internal
[[Prototype]]toUser.prototype. - Runs the constructor with
thisbound to that object. - Returns the object, unless the constructor explicitly returns another object.
const user = new User('Maria');
console.log(Object.getPrototypeOf(user) === User.prototype); // true
console.log(user.constructor === User); // true
console.log(Object.keys(user)); // ['name'], data onlyThe instance stores only its own data and takes behaviour from the prototype.
Methods are properties of prototype
Every method declared in a class body automatically goes to Class.prototype, so all instances share one function in memory.
console.log(Object.getOwnPropertyNames(User.prototype));
// ['constructor', 'sayHi']
const u1 = new User('Maria');
const u2 = new User('Oleh');
console.log(u1.sayHi === u2.sayHi); // true, one method for everybodyThis is the core idea of prototypes: do not duplicate functions in every object. The exceptions are class fields (name = 'default') and methods declared as arrow function properties: those are created per instance.
extends is prototypal inheritance
extends does nothing new, it only links prototypes.
class Animal {
speak() {
console.log('The animal makes a sound');
}
}
class Dog extends Animal {
bark() {
console.log('Woof woof!');
}
}
const d = new Dog();
d.speak(); // 'The animal makes a sound'Under the hood this is roughly what happens:
// links the instance prototypes: methods
Object.setPrototypeOf(Dog.prototype, Animal.prototype);
// links the constructors themselves: static members
Object.setPrototypeOf(Dog, Animal);Hence two parallel chains. For instances:
d -> Dog.prototype -> Animal.prototype -> Object.prototype -> null
For constructors:
Dog -> Animal -> Function.prototype -> Object.prototype -> null
You can confirm all of it in code:
console.log(Object.getPrototypeOf(d) === Dog.prototype); // true
console.log(Object.getPrototypeOf(Dog.prototype) === Animal.prototype); // true
console.log(Object.getPrototypeOf(Animal.prototype) === Object.prototype); // true
console.log(Object.getPrototypeOf(Dog) === Animal); // trueThe second chain explains why parent static methods are reachable through the child class: Dog.someStaticMethod() is found on Animal.
How a method is looked up and what instanceof checks
A d.speak() call runs like this:
- Does
ditself have an ownspeakproperty? No. - The engine looks in
Dog.prototype. No. - Then in
Animal.prototype. Found, called withthis === d. - If it were still missing, the search would continue to
Object.prototypeand finally yieldundefined.
instanceof walks the same chain: it checks whether Constructor.prototype appears among the object prototypes.
console.log(d instanceof Dog); // true
console.log(d instanceof Animal); // true, Animal.prototype is in the chain
console.log(d instanceof Object); // trueThe super keyword also works through prototypes: super.speak() calls the method found on Object.getPrototypeOf(Dog.prototype) with the current this, while super(...) in a constructor invokes the parent constructor, which is what creates this for a derived class.
What a class does under the hood, and how it still differs
The equivalent without class looks like this:
function Animal() {}
Animal.prototype.move = function () {};
function Dog() {
Animal.call(this);
}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
Dog.prototype.bark = function () {};
const dog = new Dog();
dog.bark();| Class construct | What happens under the hood |
|---|---|
constructor | the body of the constructor function |
| class method | a property on Class.prototype |
| static method | a property on the Class function itself |
| class field | an own instance property created in the constructor |
extends | Child.prototype points to Parent.prototype, Child to Parent |
super() | a call to the parent constructor, which creates this |
instanceof | a prototype chain check |
The sugar is not entirely transparent though, and interviewers ask about these differences:
- Class code always runs in strict mode.
- Class methods are non enumerable, so they never show up in
for...in. - A class cannot be called without
new: doing so throws aTypeError. - A class declaration is not hoisted the way a
functionis: before its line it sits in the TDZ. - In a derived class
thisis unavailable untilsuper()has been called.
Common mistakes
- Treating a class as a separate OOP model. JavaScript has no classes in the Java or C# sense: there are only objects and prototype links.
- Confusing
User.prototypewithObject.getPrototypeOf(User). The first is the prototype of future instances, the second is the prototype of the class function itself, which is where static members come from. - Thinking methods are copied into the instance. They live on one shared prototype, which is why
u1.sayHi === u2.sayHi. - Calling a class without
new. An old style constructor function tolerates it (and corruptsthis), while a class throws aTypeError. - Assigning arrow functions to fields without a reason.
handle = () => {}creates a new function per instance and never reaches the prototype, so memory grows. - Forgetting
super()in a derived constructor. Without it, touchingthisthrows aReferenceError, because the object does not exist yet.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.