JavaScript has no classes underneath. It has objects that point at other objects. class is a nicer way to write that arrangement, not a different one.
Interviewers ask about this constantly, because understanding the prototype chain is what separates someone who has used class from someone who knows what it does.
The prototype chain
In simple words: every object has a hidden link to another object. When you ask for a property that is not there, JavaScript follows that link and asks the next one. It keeps going until it finds the property or runs out of links.
That hidden link is the object's prototype.
prototype versus __proto__
These two names cause most of the confusion, and they are not the same thing.
| What it is | Lives on | |
|---|---|---|
prototype | the object that will become the prototype of instances made with new | functions only |
__proto__ | the actual link to this object's prototype | every object |
function Dog(name) { this.name = name; }
Dog.prototype.bark = function () { return `${this.name} says woof`; };
const rex = new Dog("Rex");
rex.__proto__ === Dog.prototype; // true
Object.getPrototypeOf(rex) === Dog.prototype; // true, and this is the modern way
rex.prototype; // undefined -- instances have no `prototype`
Dog.prototype.constructor === Dog; // truenew, and how to implement it
In simple words: new does four things. Knowing them is how you answer "what does new actually do?".
- Create an empty object.
- Set that object's prototype to the constructor function's
prototype. - Run the constructor with
thisbound to the new object. - Return the new object — unless the constructor explicitly returned an object of its own.
function myNew(Constructor, ...args) {
// 1 and 2
const instance = Object.create(Constructor.prototype);
// 3
const result = Constructor.apply(instance, args);
// 4 -- an explicitly returned object wins, anything else is ignored
return result !== null && typeof result === "object" ? result : instance;
}
function Dog(name) { this.name = name; }
Dog.prototype.bark = function () { return `${this.name} says woof`; };
const rex = myNew(Dog, "Rex");
rex.bark(); // "Rex says woof"
rex instanceof Dog; // trueClasses
In simple words: class is syntax over the same prototype machinery. Methods you write in a class body land on Ferrari.prototype, exactly as if you had assigned them by hand.
class Animal {
static kingdom = "Animalia"; // on the class, not on instances
#secret = "hidden"; // truly private, not just a convention
constructor(name) {
this.name = name; // own property, on each instance
}
eat() { // goes on Animal.prototype
return `${this.name} is eating`;
}
get label() { // getter, accessed without parentheses
return `Animal: ${this.name}`;
}
reveal() { return this.#secret; }
}
class Dog extends Animal {
constructor(name, breed) {
super(name); // must run before touching `this`
this.breed = breed;
}
eat() {
return `${super.eat()}, noisily`;
}
}
const rex = new Dog("Rex", "Lab");
rex.eat(); // "Rex is eating, noisily"
Object.getPrototypeOf(Dog.prototype) === Animal.prototype; // true
rex.secret; // undefined
rex.#secret; // SyntaxError -- not accessible at allWhat is genuinely new, not just sugar
Interviewers like the question "are classes just syntactic sugar?". The honest answer is mostly, but not entirely:
- Class bodies always run in strict mode.
- Class declarations are not hoisted usably — they sit in the temporal dead zone.
- Calling a class without
newthrows. A constructor function would silently misbehave. - Methods are non-enumerable, so they do not appear in
for...in. #privatefields have no equivalent in the old syntax. They are enforced by the engine.superworks through a hidden link that hand-written prototypes cannot reproduce exactly.
Prototypal versus classical inheritance
In simple words: in a classical language a class is a blueprint and objects are stamped from it. In JavaScript an object simply delegates to another object at runtime.
| Classical | Prototypal |
|---|---|
| Classes are templates, separate from instances | Prototypes are ordinary objects |
| Copying happens at instantiation | Nothing is copied — lookup delegates at runtime |
| The hierarchy is fixed at compile time | The chain can be changed while running |
| Method resolution decided ahead of time | Method resolution walks the chain on each miss |
The practical consequence: adding a method to a prototype affects every existing instance immediately, because nothing was ever copied.
const rex = new Dog("Rex");
Dog.prototype.sit = function () { return `${this.name} sits`; };
rex.sit(); // "Rex sits" -- rex was created before sit existedObject.create and friends
In simple words: Object.create makes a new object and lets you choose its prototype directly, with no constructor involved.
const animal = {
eat() { return `${this.name} is eating`; },
};
const dog = Object.create(animal); // dog.__proto__ === animal
dog.name = "Rex";
dog.eat(); // "Rex is eating"
Object.getPrototypeOf(dog) === animal; // true
// A dictionary with no inherited keys at all
const lookup = Object.create(null);
lookup.toString; // undefined -- no Object.prototypeProperty descriptors
In simple words: every property has three hidden switches besides its value. Normally they are all on. Object.defineProperty lets you turn them off.
| Switch | What it controls |
|---|---|
writable | can the value be changed |
enumerable | does it show up in Object.keys and for...in |
configurable | can it be deleted or have its descriptor changed |
const user = {};
Object.defineProperty(user, "id", {
value: 42,
writable: false,
enumerable: false,
configurable: false,
});
user.id = 99; // silently ignored in sloppy mode, throws in strict
Object.keys(user); // [] -- hidden from enumeration
JSON.stringify(user); // "{}"
user.id; // 42
Object.getOwnPropertyDescriptor(user, "id");
// { value: 42, writable: false, enumerable: false, configurable: false }freeze, seal and preventExtensions
In simple words: three levels of locking an object, from loosest to tightest.
| Add new properties | Delete properties | Change existing values | |
|---|---|---|---|
preventExtensions | no | yes | yes |
seal | no | no | yes |
freeze | no | no | no |
const config = Object.freeze({ retries: 3, nested: { timeout: 5000 } });
config.retries = 5; // ignored, or throws in strict mode
config.nested.timeout = 1; // WORKS -- freeze is only one level deep
Object.isFrozen(config); // true
Object.isFrozen(config.nested); // falseDeep freeze
function deepFreeze(obj) {
for (const value of Object.values(obj)) {
if (value && typeof value === "object" && !Object.isFrozen(value)) {
deepFreeze(value);
}
}
return Object.freeze(obj);
}Getters, setters and the Object statics
In simple words: a getter looks like a property but runs a function. Useful for derived values that should not be stored.
const order = {
items: [{ price: 100 }, { price: 250 }],
get total() {
return this.items.reduce((sum, item) => sum + item.price, 0);
},
set discount(percent) {
if (percent < 0 || percent > 100) throw new RangeError("0 to 100 only");
this._discount = percent;
},
};
order.total; // 350 -- no parentheses, recalculated every read
order.discount = 10; // runs the setter, with validationThe statics worth knowing cold:
Object.keys(obj); // own enumerable string keys
Object.values(obj); // their values
Object.entries(obj); // [key, value] pairs -- the one you use most
Object.fromEntries(arr); // the reverse, great after a filter or map
Object.assign(a, b); // shallow merge into a, mutating it
Object.hasOwn(obj, k); // modern replacement for hasOwnProperty
// A common real pattern: filter an object by its values
const cleaned = Object.fromEntries(
Object.entries(params).filter(([, value]) => value !== undefined)
);Mixins and composition
In simple words: JavaScript has single inheritance — one prototype link per object. When you need behaviour from two places, you compose instead of inherit.
const canFly = {
fly() { return `${this.name} takes off`; },
};
const canSwim = {
swim() { return `${this.name} dives in`; },
};
class Duck {
constructor(name) { this.name = name; }
}
Object.assign(Duck.prototype, canFly, canSwim);
new Duck("Donald").fly(); // "Donald takes off"Proxy and Reflect
In simple words: a Proxy wraps an object and lets you intercept the basic operations on it — reading, writing, deleting, checking existence.
const target = { name: "Subham" };
const safe = new Proxy(target, {
get(obj, prop, receiver) {
if (!(prop in obj)) throw new ReferenceError(`No property "${String(prop)}"`);
return Reflect.get(obj, prop, receiver);
},
set(obj, prop, value, receiver) {
if (prop === "name" && typeof value !== "string") {
throw new TypeError("name must be a string");
}
return Reflect.set(obj, prop, value, receiver);
},
});
safe.name; // "Subham"
safe.missing; // ReferenceError instead of undefined
safe.name = 42; // TypeErrorReflect provides the default behaviour for each trap, so you can add a check and then hand off. Writing obj[prop] = value directly inside a set trap loses the correct receiver and breaks inheritance — Reflect.set does not.
This is not everyday code, but it powers real tools: Vue 3 reactivity, Immer, and most mocking libraries are built on Proxy.
Ten questions
- 01What is the difference between
prototypeand__proto__?prototypeis a property on functions. It holds the object that will become the prototype of any instance created withnew.protoexists on every object and is the actual link to that object's prototype. Sorex.proto === Dog.prototype. UseObject.getPrototypeOfinstead ofprotoin real code. - 02Describe exactly what
newdoes, in four steps.Creates an empty object. Sets its prototype to the constructor's
prototypeproperty. Calls the constructor withthisbound to that object. Returns the object, unless the constructor explicitly returned an object of its own, in which case that one wins.A returned primitive is ignored — you still get the instance.
- 03Are ES6 classes just syntactic sugar over prototypes?
Mostly, but not entirely. Methods still live on the prototype and inheritance is still a prototype link.
What is genuinely new: class bodies are always strict mode, the binding sits in the temporal dead zone, calling a class without
newthrows, methods are non-enumerable, and#privatefields are enforced by the engine rather than being a naming convention. - 04You add a method to
Dog.prototypeafter creating an instance. Can that instance use it? Why?Yes, immediately. Nothing was ever copied into the instance — the instance holds a live link to the prototype object, and lookup happens at the moment you access the property.
This is the core difference from classical inheritance, where the shape is fixed when the object is created.
- 05Why would you use
Object.create(null)instead of?To get an object with no prototype at all, so it inherits nothing — no
toString, noconstructor, noproto.It matters for lookup tables keyed by user input, where a key named
protoorconstructorwould otherwise collide with inherited members. That collision is the basis of prototype pollution attacks. AMapis usually the better answer. - 06What do
writable,enumerableandconfigurablecontrol?writable— whether the value can be reassigned.enumerable— whether it appears inObject.keys,for...inandJSON.stringify.configurable— whether it can be deleted or have its descriptor changed.They all default to
truefor a normal assignment and tofalsewhen you useObject.defineProperty. - 07What is the difference between
freeze,sealandpreventExtensions?preventExtensionsblocks adding properties.sealalso blocks deleting them.freezealso blocks changing their values.All three are one level deep. A frozen object's nested objects are still fully mutable, which is why you need a recursive
deepFreezeif you want real immutability. - 08Why is extending
Array.prototypeconsidered dangerous?Because nothing is copied — the change is immediately visible to every array in the process, including arrays created inside third-party libraries.
An enumerable addition also appears in every
for...inloop over an array, and if two libraries add the same method name with different behaviour, one silently overwrites the other. - 09What does "composition over inheritance" mean here, concretely?
JavaScript objects have a single prototype link, so multiple inheritance is not available. Instead you attach independent behaviours — often by assigning mixin objects onto a prototype, or by having an object hold collaborators rather than extend them.
The practical benefit is that each behaviour can be tested, reused and removed on its own, instead of being locked into a hierarchy where a change near the top affects every descendant.
- 10Why use
Reflect.setinside a Proxysettrap rather than assigning directly?Reflect.setperforms the default operation while correctly forwarding thereceiver, which matters when the property is accessed through an inheriting object or a setter further up the chain.Assigning with
obj[prop] = valueinside the trap loses that receiver and can either break inheritance or trigger the trap again and recurse.
Six exercises
- 01
Implement myNew(Constructor, ...args)
Replicate all four steps of
new. Prove it works by checkinginstanceofand by writing a constructor that explicitly returns an object, then one that returns a number — the behaviours must differ. - 02
Build the same class twice
Write an
Animalwith aDogsubclass using ES6class. Then write the identical structure using only constructor functions,Object.createand explicit prototype assignment. Note every difference you had to work around. - 03
Write deepFreeze
Freeze every level. Then make it survive an object that contains a circular reference — you will need to track what you have already visited.
- 04
Hide a property completely
Use
Object.definePropertyto add anidthat cannot be changed, does not appear inObject.keys, and is skipped byJSON.stringify. Then find the one method that still reveals it. - 05
Compose instead of inherit
Model a
Duckthat can both fly and swim, without any inheritance chain. Then add aPenguinthat swims but does not fly, and show why an inheritance-based version would have been awkward. - 06
Validating Proxy
Wrap a plain object so that reading a missing property throws instead of returning undefined, and writing a wrong type throws. Use
Reflectfor the default behaviour, and explain in a comment why direct assignment would be wrong.