Constructor Functions
Mental model: new is four steps: make an empty object, link it to Fn.prototype, run Fn with that object as this, hand it back unless the function returned an object of its own.
Level: intermediate · about 14 minutes
function Point(x, y) {
this.x = x;
this.y = y;
}
Point.prototype.length = function () {
return Math.hypot(this.x, this.y);
};
const p = new Point(3, 4);
console.log(p.x, p.y); // 3 4
console.log(p.length()); // 5
console.log(p instanceof Point); // trueA constructor is an ordinary function. Only the call site makes it special.
Nothing in the body of Point says "I am a constructor". It assigns to this and returns nothing. The new keyword is what supplies a fresh this, links it to Point.prototype, and returns it for you. Capitalising the name is a convention that warns other humans, and nothing more.
What new Fn(...) does, step by step
- 1. Create a new empty object A brand new object with no own properties. You cannot see this step from the outside, but it is where
thiscomes from. - 2. Link it to
Fn.prototypeThe object’s[[Prototype]]is set to whateverFn.prototypeis at call time. This is how instances get shared methods. - 3. Call
Fnwiththisset to the new object Rule 1 of the binding rules. Arguments are forwarded unchanged. Assignments tothis.xbecome own properties of the instance. - 4. Return the object, unless the body returned another one If the function returns an object (or a function), that value wins and your fresh object is discarded. Any primitive return value, including
undefined, is ignored.
function construct(Fn, args = []) {
const obj = Object.create(Fn.prototype); // steps 1 and 2
const out = Fn.apply(obj, args); // step 3
return out !== null && typeof out === 'object' ? out : obj; // step 4
}
function Tag(name) { this.name = name; }
Tag.prototype.shout = function () { return this.name.toUpperCase(); };
const made = construct(Tag, ['ada']);
console.log(made.shout(), made instanceof Tag); // 'ADA' trueReimplement new in four lines and check it against the real thing.
function Sneaky() {
this.mine = 1;
return { theirs: 2 }; // an object, so it wins
}
function Ignored() {
this.mine = 1;
return 42; // a primitive, so it is discarded
}
console.log(new Sneaky()); // { theirs: 2 } - and NOT an instanceof Sneaky
console.log(new Ignored()); // Ignored { mine: 1 }
console.log(new Sneaky() instanceof Sneaky); // false
Forgetting new
Call a constructor without new and none of the four steps happen. It becomes a plain call, so default binding applies. In a module or any strict code this is undefined, so the first assignment throws. In sloppy code it is worse: this is globalThis, the assignments silently create globals, and the function returns undefined.
Sloppy script: silent corruption
function User(name) {
this.name = name; // this === globalThis
}
const u = User('Ada'); // no new
u; // undefined
globalThis.name; // 'Ada' - leaked
Strict code: a loud TypeError
'use strict';
function User(name) {
this.name = name; // this === undefined
}
User('Ada');
// TypeError: Cannot set properties
// of undefined (setting 'name')The strict version is the one you want. This is a large part of why classes throw when you call them without new, and why modules are strict by default.
function User(name) {
if (!new.target) {
return new User(name); // be forgiving
}
this.name = name;
}
function Strict(name) {
if (!new.target) throw new TypeError('Strict must be called with new');
this.name = name;
}
console.log(User('Ada').name); // 'Ada' - works either way
try { Strict('Ada'); } catch (e) { console.log(e.message); }new.target is the function new was called on, or undefined for a plain call.
Methods on the prototype, data on the instance
You can define methods in two places. Inside the constructor gives every instance its own copy of the function. On the prototype gives all instances one shared copy. For behaviour, shared is what you want.
Per instance: a new closure each time
function Counter() {
let count = 0; // private
this.inc = () => ++count; // own property
}
const a = new Counter();
const b = new Counter();
a.inc !== b.inc; // true: two functions
Object.hasOwn(a, 'inc'); // true
On the prototype: one shared function
function Counter() {
this.count = 0; // public
}
Counter.prototype.inc = function () {
return ++this.count;
};
const a = new Counter();
const b = new Counter();
a.inc === b.inc; // true: one function
Object.hasOwn(a, 'inc'); // falsePer-instance methods buy you real privacy through closures, at the cost of one function object per instance per method. Prototype methods are shared and show up in the prototype chain, which is what debuggers, instanceof and subclassing all expect. Use the prototype unless you specifically need the closure.
| Where you define it | Copies | Sees closure variables | On the chain | Typical use |
|---|---|---|---|---|
Fn.prototype.m = ... | one, shared | no | yes | ordinary methods |
this.m = function () {} | one per instance | yes | no | privacy, or auto-bound handlers |
Fn.m = ... | one, on the function | no | no (statics are not inherited by instances) | factories, constants, helpers |
function Stack() { this.items = []; }
Stack.prototype.push = function (v) { this.items.push(v); return this; };
Stack.prototype.pop = function () { return this.items.pop(); };
Stack.from = function (iterable) { // a static
const s = new Stack();
for (const v of iterable) s.push(v);
return s;
};
const s = Stack.from([1, 2, 3]);
console.log(s.pop(), s.items); // 3 [1, 2]
console.log(typeof s.from); // 'undefined' - statics are not inherited
instanceof reads the chain
obj instanceof Fn does not compare constructors. It walks obj’s prototype chain looking for the exact object Fn.prototype. That has two consequences: it keeps working through any depth of inheritance, and it breaks the moment you replace Fn.prototype after an instance was made.
function A() {}
const before = new A();
A.prototype = { fresh: true }; // a different object now
const after = new A();
console.log(before instanceof A); // false - its link points at the OLD object
console.log(after instanceof A); // true
console.log(before.constructor === A); // true, so constructor and instanceof disagree
rex ──[[Prototype]]──> Dog.prototype ──> Object.prototype ──> null
^
|
is this exact object anywhere in the chain?
yes -> true, no -> keep walking, hit null -> false
function Box(v) {
this.v = v;
return { v: v * 2 };
}
const b = new Box(5);
console.log([b.v, b instanceof Box]);Step 4 of new: the body returned an object, so that object is what you get and the freshly linked instance is thrown away. b.v is 10, and because b was never linked to Box.prototype, instanceof is false.
`new.target`- the constructor
newwas invoked with,undefinedin a plain call `Fn.prototype.constructor`- back reference to
Fn, non-enumerable, easy to lose `obj instanceof Fn`- is
Fn.prototypeanywhere in obj’s chain `Reflect.construct(Fn, args)`- the built-in, spelled as a function call
Arrow functions- no
.prototype, nonew.target, cannot be constructed Class constructors- throw a TypeError if you call them without
new
A constructor function is a factory that lets
newdo the wiring.
Try it yourself
The four steps, by hand
function construct(Fn, args = []) {
const obj = Object.create(Fn.prototype);
const out = Fn.apply(obj, args);
return out !== null && typeof out === 'object' ? out : obj;
}
function User(name, role) {
this.name = name;
this.role = role;
}
User.prototype.label = function () { return `${this.name} (${this.role})`; };
const byNew = new User('Ada', 'lead');
const byHand = construct(User, ['Ada', 'lead']);
console.log(byNew.label(), byHand.label());
console.log(byHand instanceof User, Object.getPrototypeOf(byHand) === User.prototype);
Break each step in turn: skip the Object.create, ignore the return value, forget to forward the arguments. Watch which test-like behaviour dies.
Guard against a missing new
function Guarded(id) {
if (!new.target) throw new TypeError('Guarded requires new');
this.id = id;
}
console.log(new Guarded('a').id);
try {
Guarded('b');
} catch (err) {
console.log(err.constructor.name, err.message);
}
Swap the throw for the self-correcting return new Guarded(...) version. Which do you prefer in application code, and which in a library?
Exercises
Implement new
Write construct(Fn, args = []) that behaves like new Fn(...args) without using the new keyword or Reflect.construct. Link the fresh object to Fn.prototype, call Fn with it as this, forward the arguments, and respect the return rule: an object returned by the body wins, a primitive return value is ignored.
A queue in the pre-class idiom
Write a constructor function Queue(...items) in the old style. Each instance owns its own items array. enqueue(v) adds to the back and returns the queue so calls chain, dequeue() removes and returns the front item (or undefined when empty), and size() returns the count. All three methods must live on Queue.prototype, shared by every instance, and calling Queue(...) without new must throw a TypeError.
Check yourself
- What does this log?
- 'v2:1' —
aholds a live link to theWidget.prototypeobject, and you replaced a property on that object rather than replacing the object itself. The lookup happens at call time, so it finds the new function. ReassigningWidget.prototypeto a whole new object would have leftapointing at the old one. - Which of the four steps does
newskip when the constructor body returns an object? - It performs all four steps but discards the fresh object at step 4 — All the work still happens: the object is created, linked and passed as
this. Step 4 then throws it away in favour of the returned object, which is why the result is not an instance of the constructor. - In strict code, what happens when you call a constructor function without
new? thisisundefined, so the first assignment tothis.xthrows a TypeError — A plain call means default binding, and strict code leavesthisasundefinedinstead of coercing it.undefined.x = ...throws. The sloppy-mode version silently writes to the global object, which is the bugnew.targetguards exist to prevent.- Why put methods on
Fn.prototyperather than assigning them inside the constructor? - One shared function object serves every instance, and it lives on the chain where
instanceof, subclassing and debuggers expect it — The observable differences are memory (one function versus one per instance) and location (Object.hasOwn(a, "m")is false for a prototype method). Per-instance functions do buy closure privacy, which is a real reason to choose them, just not the default one.
Common mistakes
- Calling a constructor without
newand, in sloppy code, quietly writing to the global object. - Returning an object from a constructor by accident, which silently replaces the instance.
- Replacing
Fn.prototypewholesale and losing theconstructorback reference, or orphaning instances created earlier. - Putting mutable data such as an array on the prototype, so every instance shares it.
- Using an arrow function as a prototype method: it has no
thisof its own, so the receiver is lost. - Expecting instances to inherit statics.
Fn.helperis not visible asinstance.helper.
Takeaways
newdoes four things: create, link toFn.prototype, call with the object asthis, return it unless the body returned an object.- A constructor function is an ordinary function; the capital letter is only a convention.
- Without
newyou get default binding, which is a TypeError in strict code and a global leak in sloppy code. new.targetlets you detect the difference and either throw or self-correct.- Shared behaviour goes on the prototype, per-instance data goes on
this. instanceofwalks the chain looking forFn.prototype, so replacing that object changes the answer.