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);  // true

A 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. 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 this comes from.
  2. 2. Link it to Fn.prototype The object’s [[Prototype]] is set to whatever Fn.prototype is at call time. This is how instances get shared methods.
  3. 3. Call Fn with this set to the new object Rule 1 of the binding rules. Arguments are forwarded unchanged. Assignments to this.x become own properties of the instance.
  4. 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' true

Reimplement 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');         // false

Per-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 itCopiesSees closure variablesOn the chainTypical use
Fn.prototype.m = ...one, sharednoyesordinary methods
this.m = function () {}one per instanceyesnoprivacy, or auto-bound handlers
Fn.m = ...one, on the functionnono (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 new was invoked with, undefined in a plain call
`Fn.prototype.constructor`
back reference to Fn, non-enumerable, easy to lose
`obj instanceof Fn`
is Fn.prototype anywhere in obj’s chain
`Reflect.construct(Fn, args)`
the built-in, spelled as a function call
Arrow functions
no .prototype, no new.target, cannot be constructed
Class constructors
throw a TypeError if you call them without new

A constructor function is a factory that lets new do the wiring.

The one-line version

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' — a holds a live link to the Widget.prototype object, 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. Reassigning Widget.prototype to a whole new object would have left a pointing at the old one.
Which of the four steps does new skip 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?
this is undefined, so the first assignment to this.x throws a TypeError — A plain call means default binding, and strict code leaves this as undefined instead of coercing it. undefined.x = ... throws. The sloppy-mode version silently writes to the global object, which is the bug new.target guards exist to prevent.
Why put methods on Fn.prototype rather 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 new and, in sloppy code, quietly writing to the global object.
  • Returning an object from a constructor by accident, which silently replaces the instance.
  • Replacing Fn.prototype wholesale and losing the constructor back 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 this of its own, so the receiver is lost.
  • Expecting instances to inherit statics. Fn.helper is not visible as instance.helper.

Takeaways

  • new does four things: create, link to Fn.prototype, call with the object as this, return it unless the body returned an object.
  • A constructor function is an ordinary function; the capital letter is only a convention.
  • Without new you get default binding, which is a TypeError in strict code and a global leak in sloppy code.
  • new.target lets you detect the difference and either throw or self-correct.
  • Shared behaviour goes on the prototype, per-instance data goes on this.
  • instanceof walks the chain looking for Fn.prototype, so replacing that object changes the answer.