Emmanuel Akinfulubi

Lesson 05 of 1513 min read

Constructors and new

The older way JavaScript made objects, and the one class is built on top of. Four things happen when you write new. Knowing all four is what makes class stop feeling like magic.

By the end: Explain exactly what new does, step by step.

You will not write many constructor functions in new code. You need to understand them completely anyway, because class is a thin layer over exactly this, and every confusing thing about classes is explained here.

A constructor function is an ordinary function that you call with new. The convention is to capitalise its name, which is the only signal a reader gets.

A constructor, and the object it produces

function Bus(plate, seatsLeft) {
  this.plate = plate;
  this.seatsLeft = seatsLeft;
}

const lagos = new Bus('LAG-441-XA', 9);
console.log(lagos);
console.log(lagos.plate, lagos.seatsLeft);

Output


      

Notice what is missing: there is no return. The function assigns to this and stops. Something else is doing the returning.

The four things new does

You can do all four by hand. Doing it once removes the mystery permanently.

new, written out longhand

function Bus(plate, seatsLeft) {
  this.plate = plate;
  this.seatsLeft = seatsLeft;
}

function pretendNew(Ctor, ...args) {
  const obj = Object.create(Ctor.prototype);   // steps 1 and 2
  const result = Ctor.apply(obj, args);        // step 3
  return typeof result === 'object' && result !== null ? result : obj;  // step 4
}

const real = new Bus('LAG-441-XA', 9);
const byHand = pretendNew(Bus, 'LAG-441-XA', 9);

console.log(real);
console.log(byHand);
console.log('same shape:', JSON.stringify(real) === JSON.stringify(byHand));
console.log('both instances of Bus:', real instanceof Bus, byHand instanceof Bus);

Output


      

What happens if you forget new

This used to be a real trap. Without new, the function is just a function: there is no new object, and this is not what you expect.

Forgetting new, in strict mode

'use strict';

function Bus(plate) {
  this.plate = plate;
}

try {
  const oops = Bus('LAG-441-XA');   // no new
  console.log(oops);
} catch (err) {
  console.log('Threw:', err.name + ':', err.message);
}

Output


      

Strict mode makes that a loud error, which is what you want. In old sloppy-mode code this was the global object, so Bus('LAG-441-XA') silently created a global variable called plate and returned undefined. Hours of debugging lived in that gap.

A constructor can defend itself with new.target, which is undefined unless the function was called with new.

A constructor that insists

function Bus(plate) {
  if (!new.target) {
    throw new TypeError('Bus must be called with new.');
  }
  this.plate = plate;
}

console.log(new Bus('LAG-441-XA'));

try {
  Bus('ABJ-772-KM');
} catch (err) {
  console.log('Refused:', err.message);
}

Output


      

Where to put the methods

Do it the obvious way and every instance gets its own copy, exactly like a factory.

Methods on this: one copy per object

function Bus(plate) {
  this.plate = plate;
  this.describe = function () {
    return 'I am ' + this.plate;
  };
}

const a = new Bus('A');
const b = new Bus('B');

console.log(a.describe());
console.log('same function object?', a.describe === b.describe);
console.log('own keys:', Object.keys(a));

Output


      

Now put the method on Bus.prototype instead. Step 2 of new linked every instance to that object, so every instance can reach it, and there is exactly one copy.

Methods on the prototype: one copy, shared

function Bus(plate, seatsLeft) {
  this.plate = plate;
  this.seatsLeft = seatsLeft;
}

Bus.prototype.describe = function () {
  return this.plate + ' has ' + this.seatsLeft + ' seats left';
};

Bus.prototype.book = function (n) {
  this.seatsLeft -= n;
  return this;
};

const a = new Bus('LAG-441-XA', 9);
const b = new Bus('ABJ-772-KM', 6);

console.log(a.describe());
console.log(b.describe());
console.log('same function object?', a.describe === b.describe);

// The instance owns only its data. The behaviour lives one level up.
console.log('own keys:', Object.keys(a));
console.log('but describe is reachable:', typeof a.describe);

console.log(a.book(2).describe());

Output


      

instanceof, and what it really asks

Checking what built an object

function Bus(plate) { this.plate = plate; }
function Truck(plate) { this.plate = plate; }

const b = new Bus('LAG-441-XA');

console.log('b instanceof Bus:', b instanceof Bus);
console.log('b instanceof Truck:', b instanceof Truck);
console.log('b instanceof Object:', b instanceof Object);

// constructor is a property that lives on the prototype
console.log('built by:', b.constructor.name);

Output


      

instanceof does not compare names or check which function ran. It asks: is Bus.prototype anywhere on this object's prototype chain? That distinction matters in the next lesson, and it is why instanceof keeps working across inheritance.

The escape hatch in step four

Step 4 said the new object is returned unless the function returns a different object. Returning a primitive is ignored. Returning an object overrides everything.

Returning from a constructor

function IgnoredReturn(plate) {
  this.plate = plate;
  return 42;               // primitive: ignored
}

function Hijacked(plate) {
  this.plate = plate;
  return { plate: 'something else entirely' };   // object: wins
}

console.log(new IgnoredReturn('LAG-441-XA'));
console.log(new Hijacked('LAG-441-XA'));
console.log('still an instance?', new Hijacked('x') instanceof Hijacked);

Output


      

Your turn: constructor plus prototype

Write a Stop constructor taking town and minutes. Put two methods on Stop.prototype: describe() returning something like "Ore at 90 min", and delay(n) which adds to minutes and returns this. Then confirm both instances share one copy of describe.

Editable. Fill it in, then run it.

Output


      

Recap

  • A constructor is a normal function called with new, capitalised by convention only.
  • new does four things: makes an object, links its prototype, runs the function with this set to it, and returns it.
  • Without new in strict mode you get a TypeError; new.target lets a function detect the mistake.
  • Put data on this and methods on Ctor.prototype, so all instances share one copy of each method.
  • instanceof asks whether a prototype is on the object's chain, not which function ran.

Saved in this browser only.

Newsletter

New writing, when it lands.

Occasional notes on shipping software with AI agents. No schedule, no filler, unsubscribe in one click.