The new keyword performs four specific steps, and knowing them turns constructor behaviour from a rule into something you can derive.
Complete lesson & earn 250 PX
EXERCISE
1Everything new does can be written out in ordinary JavaScript, which is the fastest way to stop it feeling like magic.
EXERCISE
2A constructor that returns an object overrides everything new was building, and that rule has real consequences.
EXERCISE
3Calling a constructor without new does not fail loudly; it quietly does something else, and the language has grown several answers to that.
new is a keyword, which makes it look like something the engine does that you could not. It is not. Everything it does can be written in four lines of ordinary JavaScript, and writing them is the most reliable way to stop guessing about constructor behaviour.
What new Person("Ada") does, in order:
// 1. Create a brand new empty object.
// 2. Set that object's __proto__ to Person.prototype.
// 3. Call Person with `this` bound to that object, passing the arguments.
// 4. Return the object, UNLESS the function itself returned an object.
Step 2 is the link from lesson 2, installed. Step 3 is why writing this.name = name inside a constructor puts a property on the instance. Step 4 is a rule that surprises people and gets its own exercise next.
The same four steps, written by hand:
function Person(name) {
this.name = name;
}
Person.prototype.greet = function () {
return `Hi, ${this.name}`;
};
function myNew(Constructor, ...args) {
const obj = Object.create(Constructor.prototype); // steps 1 and 2
const result = Constructor.apply(obj, args); // step 3
return typeof result === "object" && result !== null // step 4
? result
: obj;
}
const a = new Person("Ada");
const b = myNew(Person, "Ada");
console.log(a.name, b.name);
.(b.());
.(.(b) === .);
Both objects are indistinguishable. One was made by a keyword, the other by a function you can read.
One line needs explaining ahead of time. Constructor.apply(obj, args) calls the function with this set to obj and the array spread out as arguments. That is Unit 2's subject, arriving one unit early because new genuinely cannot be described without it. For now you only need the summary: apply runs a function and lets you choose what this will be inside it.
You can also watch each step land from inside the constructor:
function Person(name) {
console.log(this); // Person {}
console.log(Object.getPrototypeOf(this) === Person.prototype); // true
this.name = name;
console.log(this); // Person { name: 'Ada' }
// no return statement, so step 4 hands back `this`
}
new Person("Ada");
By the time your first line of constructor code runs, steps 1 and 2 have already happened. The object exists and is already linked. Your job inside a constructor is only ever step 3: fill in the per-instance data.
That reframing is worth holding on to. A constructor is not a function that makes an object. It is a function that is handed an object and asked to set it up.
Nothing in that list requires engine support you could not write yourself, which is exactly why class can honestly be called sugar. It is these same four steps with tidier syntax and some guard rails bolted on. If you can write myNew, there is no part of construction in left to take on faith.
Step 4 said the new object is returned unless the function returned an object of its own. That exception is easy to skim past and it has consequences you will meet in real code.
A primitive return is ignored completely:
function Person(name) {
this.name = name;
return 42;
}
const p = new Person("Ada");
console.log(p); // Person { name: 'Ada' }
console.log(p === 42); // false
Numbers, strings, booleans, null, undefined: all discarded. new hands back the object it built, as though the return were not there.
An object return replaces the instance entirely:
function Person(name) {
this.name = name;
return { hijacked: true };
}
const p = new Person("Ada");
console.log(p); // { hijacked: true }
console.log(p.name); // undefined
console.log(p instanceof Person); // false
Everything new prepared is thrown away. The prototype link goes with it, which is why instanceof now says no. The work in step 3 still happened, it just happened to an object nobody kept.
Which is not only a hazard. It is how a singleton is written without a class:
let instance = null;
function Config() {
if (instance) return instance; // step 4 hands this back instead
this.createdAt = Date.now();
instance = this;
}
const a = new Config();
const b = new Config();
console.log(a === b); // true
However many times anyone writes new Config(), they get the same object. The first call falls through and stores this; every later call returns early with an object, and step 4 honours it. A deliberate, useful application of the rule.
And here is where it bites accidentally:
function Collection() {
this.type = "collection";
return [];
}
console.log(new Collection()); // []
console.log(new Collection().type); // undefined
function Loader() {
this.ready = false;
return Promise.resolve("done");
}
console.log(new Loader() instanceof Loader); // false
Arrays are objects. Promises are objects. So are dates, maps, sets and functions. Anyone who returns one of those from a constructor, perhaps to be helpful, or because the function was refactored from a plain factory and the new at the call sites was never removed, silently loses the instance and its whole prototype chain. Nothing throws. The bug shows up later as a missing method.
Pro Tip: Constructors exist to build
this, not to produce a value. If you find yourself writingreturninside one for any reason other than an early exit, you almost certainly wanted a plain factory function instead: clearer at the call site, nonewto forget, and none of this rule attached to it.
A constructor is an ordinary function. Nothing about function Person(name) marks it as requiring new. Leave the keyword off and the function still runs, which historically made this one of the nastiest silent bugs in the language.
What used to happen:
function Person(name) {
this.name = name;
}
const bad = Person("Ada"); // no `new`
console.log(bad); // undefined
The function returned nothing, so bad is undefined. Worse, in the non-strict code of the time this inside a plain function call was the global object, so this.name = name wrote a global variable called name. A single missing keyword created a global and returned nothing, and the program carried on until something unrelated broke much later.
Strict mode converts it into an immediate error, which is the entire point:
"use strict";
function Person(name) {
this.name = name;
}
Person("Ada");
// TypeError: Cannot set properties of undefined (setting 'name')
In strict mode, this in a plain function call is undefined rather than the global object, so the assignment fails on the spot. You get a stack pointing at the real mistake instead of a corrupted global.
This matters more than it sounds, because ES module files are strict by default. Any modern import/export file already behaves this way without the directive. The old silent failure mostly belongs to script tags and legacy bundles now.
Libraries that had to support both wrote a guard:
function Person(name) {
if (!new.target) return new Person(name);
this.name = name;
}
console.log(Person("Ada").name); // Ada
console.log(new Person("Ada").name); // Ada
new.target holds the constructor function when new was used and undefined otherwise, so the function can detect how it was called and correct it. You will still see this in older library source, and now you can read it.
And how the modern language settles the question for good:
class Person {
constructor(name) {
this.name = name;
}
}
// Person("Ada");
// TypeError: Class constructor Person cannot be invoked without 'new'
A class simply refuses. Not a guard someone remembered to write, not a mode you have to opt into: a rule the language enforces at the call site, every time.
This is a concrete reason to prefer class even though it is sugar over everything in this unit. The syntax is tidier, but the real gain is that a whole category of silent failure becomes an error you cannot ship past. Unit 3 covers what else it adds, and none of it will be a new concept, only a cleaner way to spell the ones you now understand.
new creates an empty object, links it to the function's .prototype, calls the function with this bound to it, and returns it unless the function returns an object of its own. You can write all four steps yourself.