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.
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.
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.
'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.
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.
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.
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
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.
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.
Output
Recap
- A constructor is a normal function called with
new, capitalised by convention only. newdoes four things: makes an object, links its prototype, runs the function withthisset to it, and returns it.- Without
newin strict mode you get aTypeError;new.targetlets a function detect the mistake. - Put data on
thisand methods onCtor.prototype, so all instances share one copy of each method. instanceofasks whether a prototype is on the object's chain, not which function ran.