Prototypes, classes, and what new actually does
Property lookup walks a chain of objects. Classes are syntax over that chain — and new is four steps you can write yourself.
After this lesson you can
- Trace a property lookup through the prototype chain
- Tell an own property from an inherited one
- Explain what a class compiles down to and what new does, step by step
- Use extends and super correctly, including the super-before-this rule
Every object has a hidden link to another object, its prototype
(Object.getPrototypeOf(obj) reads it). When you read obj.x and obj
has no own property x, the engine looks on the prototype, then on the
prototype's prototype, until it finds x or reaches null. Writing
obj.x = 1 normally creates or updates an own property on obj, which
then shadows anything further up. (Two exceptions: if the chain has a
setter for x, the setter runs instead, and if it has a read-only x,
the write fails.)
const animal = { legs: 4, describe() { return `${this.name}, ${this.legs} legs`; } };
const bird = Object.create(animal); // bird's prototype is animal
bird.name = "Robin";
bird.legs = 2; // shadows animal.legs
bird.describe(); // "Robin, 2 legs"
describe was found on animal, but this is still bird — the
object the method was called on (lesson 4). That is the whole trick
behind shared methods.
key in obj is true for own and inherited keys; Object.hasOwn(obj, key) only for own ones. Object.keys and JSON.stringify see only own,
enumerable properties.
A class is a function plus a prototype object
class Point {
constructor(x, y) { this.x = x; this.y = y; }
describe() { return `(${this.x}, ${this.y})`; }
}
Point is a function (typeof Point === "function"). Methods go on
Point.prototype, created once and shared by every instance. Fields set
in the constructor go on each instance. A class differs from a plain
constructor function in a few deliberate ways: calling it without new
throws, its body is always strict, and its methods are not enumerable.
class Point { constructor(x, y) { this.x = x; this.y = y; } describe() { return `(${this.x}, ${this.y})`; }} export function run() { const a = new Point(1, 2); const b = new Point(3, 4); let callWithoutNew; try { Point(5, 6); } catch (e) { callWithoutNew = e.name; } return { typeofPoint: typeof Point, protoIsPointPrototype: Object.getPrototypeOf(a) === Point.prototype, sharedMethod: a.describe === b.describe, ownKeys: Object.keys(a), describeInA: "describe" in a, describeOwnByA: Object.hasOwn(a, "describe"), toStringInA: "toString" in a, callWithoutNew, };}What new does
new Point(1, 2) performs four steps:
- Create an empty object whose prototype is
Point.prototype. - Call
Pointwiththisset to that object and the given arguments. - If the call returned an object, the result of
newis that object. - Otherwise (no return, or a primitive), the result is the object from step 1.
Step 3 surprises people, and it is exactly what you implement in the exercise.
extends and super
class Admin extends User links Admin.prototype to User.prototype,
so instances find User methods by the same chain lookup. Inside a
subclass, super.method() calls the parent's version. A subclass
constructor must call super(...) before it touches this, because the
parent is what creates the object:
class User { constructor(name) { this.name = name; } label() { return `user ${this.name}`; }} class Admin extends User { label() { return `admin, ${super.label()}`; }} class Broken extends User { constructor(name) { this.role = "x"; super(name); }} export function run() { let broken; try { new Broken("Ana"); } catch (e) { broken = `${e.name}: ${e.message}`; } const admin = new Admin("Nino"); return { adminLabel: admin.label(), adminIsUser: admin instanceof User, broken, };}Private fields (#balance) are a different mechanism: they live on the
instance but are not properties at all — Object.keys, JSON.stringify
and "balance" in obj never see them — and they are reachable only from
code inside the class body that declared them, not even from a subclass.
Common mistakes
- Using
into check whether an object "has" a key."toString" in {}istrue; useObject.hasOwnwhen inherited keys must not count. - Touching
thisbeforesuper()in a subclass. It throws aReferenceError, as the second example shows. - Defining methods inside the constructor (
this.describe = () => ...). It works, but every instance gets its own copy instead of one shared method on the prototype. - Assigning to an inherited property and expecting the parent to change. The write creates an own property on the child and shadows the parent.
What interviewers ask
- Explain prototypal inheritance. — Objects delegate failed property
lookups to their prototype, up a chain ending at
null; classes set up that chain for you.
Exercise · Pro
Write new yourself
Exercises are free in the first 3 lessons of every course. From here on they are part of Pro and Sprint; the lesson and its runnable examples stay open.
Upgrade