JAVASCRIPT / PROTOTYPES, CLASSES, AND INHERITANCE
Constructor functions and the new keyword
Build objects with constructor functions, explain the four things new does to a call, and place shared methods on Fn.prototype instead of on each instance.
What you will learn
- Trace the four steps new performs, including when it discards the object it just made
- Attach methods to Fn.prototype so every instance shares one function object
- Detect a missing new with new.target and repair the call instead of corrupting state
- Keep Fn.prototype.constructor intact when you replace the prototype object
Understanding Constructor functions and the new keyword
A constructor function is an ordinary function; nothing in its definition marks it as special, and the capital initial is a signal to humans, not to the engine. All the behaviour comes from the new operator: it creates a fresh object whose internal prototype link points at the function's prototype property, calls the function with this bound to that object, and evaluates to that object. That is why the body only ever assigns own properties onto this and does not need a return statement.
Every normal function is created with a prototype property holding an object, and that object has a constructor property pointing back to the function. Fn.prototype is not the function's own prototype; it is the object that will sit behind each instance. Because all instances end up sharing that one object, a function stored on it exists once in memory no matter how many instances you make, which is the reason methods belong there and per-object data belongs on this.
The return rule is where new surprises people. If the body returns an object (or a function), new hands back that value and throws away the object it built, losing the prototype link; a primitive return, including the implicit undefined, is ignored. Calling a constructor without new does not fail at the call site: this becomes undefined under strict mode, so the first assignment throws a TypeError, while in sloppy mode this is globalThis and the assignments quietly create globals as the call returns undefined. Inside the body, new.target tells you which of the two happened.
function Point(x, y) {
this.x = x;
this.y = y;
}
Point.prototype.magnitude = function () {
return Math.hypot(this.x, this.y);
};
const p = new Point(3, 4);
console.log(p.x + "," + p.y);
console.log(p.magnitude());
console.log(Object.getPrototypeOf(p) === Point.prototype);
console.log(p.constructor === Point);
console.log(p.hasOwnProperty("magnitude"), "magnitude" in p);new is a call protocol rather than a property of the function: it makes an object linked to Fn.prototype, runs the body with this bound to it, and returns that object unless the body returns an object of its own.
Worked examples
What new actually does
Reproduces the construct step by hand so each part of the operator is visible.
function Counter(start) {
this.value = start;
}
Counter.prototype.next = function () {
this.value += 1;
return this.value;
};
function construct(Fn, args) {
const obj = Object.create(Fn.prototype);
const result = Fn.apply(obj, args);
const returnedObject =
result !== null && (typeof result === "object" || typeof result === "function");
return returnedObject ? result : obj;
}
const a = new Counter(10);
const b = construct(Counter, [10]);
console.log(a.next(), b.next());
console.log(b instanceof Counter, b.constructor === Counter);Example explained
Line 1Object.create(Fn.prototype) is the first step new takes: a new object whose prototype link is the function's prototype property.
Line 2Fn.apply(obj, args) is the second step, running the body with this bound to that object so the assignments become own properties.
Line 3The returnedObject check is the third step, the override rule: only an object or function return replaces the built object.
Line 4b behaves identically to a, which shows new adds no hidden marker to instances beyond the prototype link.
Return values and a forgotten new
Shows that an object return hijacks the result, a primitive return is discarded, and a bare call throws in strict mode.
"use strict";
function Cached(id) {
this.id = id;
return { id: "replaced" };
}
function Ignored(id) {
this.id = id;
return 42;
}
function User(name) {
this.name = name;
}
console.log(new Cached(1).id);
console.log(new Ignored(1).id);
console.log(new Cached(1) instanceof Cached);
try {
User("no new");
} catch (err) {
console.log(err.name);
}Example explained
Line 1new Cached(1).id is "replaced" because the returned literal, not the constructed object, becomes the value of the new expression.
Line 2instanceof is false for that result: the plain literal inherits from Object.prototype and was never linked to Cached.prototype.
Line 3new Ignored(1).id is 1 because the number 42 cannot replace an object, so new keeps the instance it made.
Line 4User("no new") leaves this as undefined under strict mode, so this.name = name throws a TypeError at the first assignment.
Guarding with new.target
Makes a constructor work whether or not the caller remembers new, and shows methods are shared rather than copied.
function Temperature(celsius) {
if (new.target === undefined) {
return new Temperature(celsius);
}
this.celsius = celsius;
}
Temperature.prototype.toFahrenheit = function () {
return this.celsius * 9 / 5 + 32;
};
const boiling = new Temperature(100);
const freezing = Temperature(0);
console.log(boiling.toFahrenheit(), freezing.toFahrenheit());
console.log(freezing instanceof Temperature);
console.log(boiling.toFahrenheit === freezing.toFahrenheit);Example explained
Line 1new.target holds the function new was applied to and is undefined for a plain call, so the guard can tell the two apart.
Line 2return new Temperature(celsius) is an object return, so the override rule lets the bare call produce a real instance.
Line 3freezing satisfies instanceof because the inner new created it, even though the outer call omitted the keyword.
Line 4The final true shows toFahrenheit exists once on Temperature.prototype; instances hold only celsius of their own.
Important notes
Fn.prototype is a property of the function object; an instance's link is read with Object.getPrototypeOf, and reassigning Fn.prototype afterwards never relinks objects that already exist.
Arrow functions and object-literal shorthand methods have no construct behaviour and no prototype property, so using new on them throws 'is not a constructor'.
Common mistakes
Defining methods in the body with this.next = function () { ... }: each instance gets its own copy, so a.next === b.next is false and memory grows with every object created.
Replacing the whole prototype object with Fn.prototype = { move() {} }: constructor is wiped so instance.constructor reports Object, and any instance made before the swap still points at the old prototype.
Calling the constructor without new in sloppy mode: the assignments land on globalThis and the call returns undefined, so the next property read fails with 'Cannot read properties of undefined'.
Try it yourself
Change, predict, then run
In a browser console, write a Stack constructor that sets this.items = [] and place push, pop, and size on Stack.prototype. Create two stacks and log s1.push === s2.push and s1.items === s2.items to see exactly which parts are shared.
Open the JavaScript workspaceCheck your understanding
Given function Box(v) { this.v = v; return { v: v * 2 }; } and const b = new Box(5); what are b.v and b instanceof Box?
- 5 and true
- 10 and true
- 10 and false
- A TypeError, because a constructor may not contain a return statement
Show answer
An object return overrides the construct result, so b is the literal { v: 10 } and b.v is 10. Since that literal was made by an object literal, its prototype is Object.prototype, so instanceof Box is false. '10 and true' is tempting because it assumes new keeps the prototype link and only swaps the data, but nothing re-links a returned object to Box.prototype.