What new really does
You write new Dog('Rex') thousands of times, but what does the new operator actually
do? It's not magic — it's four well-defined steps, and knowing them demystifies
constructors, the class keyword, and a whole category of bugs. When you understand new,
you understand how JavaScript turns a plain function into an object factory.
A constructor function is just a normal function intended to be called with new.
Convention capitalizes its name (Dog, not dog) to signal that intent.
The four steps of new
When you evaluate new Fn(args), the engine performs these steps:
- Create a brand-new empty object.
- Link that object's prototype to
Fn.prototype. - Call
Fnwiththisbound to the new object, passing the arguments. - Return the new object — unless the constructor explicitly returns its own object.
You can replicate it by hand to see there's nothing hidden:
function myNew(Fn, ...args) {
const obj = Object.create(Fn.prototype) // steps 1 + 2
const result = Fn.apply(obj, args) // step 3
return (typeof result === 'object' && result !== null) ? result : obj // step 4
}
function Dog(name) { this.name = name }
myNew(Dog, 'Rex') // { name: 'Rex' } same as new Dog('Rex')
That Object.create(Fn.prototype) is exactly why instances inherit the constructor's
prototype methods, and why instanceof works.
Building objects with a constructor
A constructor's job is to initialize the new object's own state via this. Shared methods
go on the prototype so all instances share them.
function Circle(radius) {
this.radius = radius // per-instance data
}
Circle.prototype.area = function () {
return Math.PI * this.radius ** 2 // shared behavior
}
const c = new Circle(5)
c.area() // 78.54...
c instanceof Circle // true
Object.getPrototypeOf(c) === Circle.prototype // true
Putting area on the prototype rather than inside the constructor means all circles share
one function object — important when you create many instances.
The return-value rule
Step 4 has a subtlety: if the constructor explicitly returns an object, that object
replaces the freshly-created this. If it returns a primitive (or nothing), the new
object is used as normal.
function A() { this.x = 1; return { x: 99 } }
new A().x // 99 — object return overrides `this` surprising
function B() { this.x = 1; return 42 }
new B().x // 1 — primitive return ignored
This is why you almost never return from a constructor. The object-override behavior is
occasionally used for factory tricks, but in normal code it's a source of confusion — let
new return this implicitly.
The forgotten-new bug
The classic disaster: calling a constructor without new. Without it, there's no new
object and this is whatever the call site provides — undefined in strict mode, or the
global object in sloppy mode. Either way, your "instance" is wrong.
function User(name) { this.name = name }
const good = new User('Ada') // proper instance
const bad = User('Grace') // in sloppy mode, sets global.name; returns undefined
bad // undefined
In sloppy mode this silently pollutes the global object; in strict mode (and inside ES
modules, which are always strict) it throws TypeError: Cannot set property 'name' of undefined, which at least fails loudly. This bug is exactly why class constructors
throw if you call them without new.
Guarding against missing new
Before class, defensive constructors guarded themselves with new.target or an
instanceof check:
function User(name) {
if (!(this instanceof User)) return new User(name) // auto-correct
this.name = name
}
User('Ada').name // 'Ada' — works with or without new
A cleaner modern tool is new.target, which is undefined when a function is called
without new and the constructor reference when called with it.
function Account(balance) {
if (new.target === undefined) {
throw new Error('Account must be called with new') // explicit guard
}
this.balance = balance
}
new.target and abstract constructors
new.target also enables "abstract" base constructors that must be subclassed, not
instantiated directly.
function Shape() {
if (new.target === Shape) {
throw new Error('Shape is abstract — extend it instead')
}
}
function Circle(r) { Shape.call(this); this.r = r }
Circle.prototype = Object.create(Shape.prototype)
new Circle(5) // fine
new Shape() // Error: Shape is abstract
Inside a subclass call, new.target is the subclass, so the abstract check only fires when
someone instantiates the base directly.
The constructor property
Every function's prototype object has a constructor property pointing back to the
function, so instances can discover what made them.
function Dog() {}
const d = new Dog()
d.constructor === Dog // true
d.constructor.name // 'Dog'
When you reassign Fn.prototype wholesale (common in manual inheritance), you wipe out this
back-pointer and must restore it: Dog.prototype.constructor = Dog. Forgetting to do so
breaks code that relies on instance.constructor.
Constructors vs factory functions
You don't have to use new. A factory function just builds and returns an object,
sidestepping this and new entirely.
function createUser(name) {
return {
name,
greet() { return `Hi, ${name}` } // closure over name, no `this` needed
}
}
const u = createUser('Ada') // no `new`, no this-binding pitfalls
Factories avoid the forgotten-new bug and the confusion around this, but each returned
object gets its own copy of every method (no shared prototype), which costs memory at scale.
Constructors (and class) trade a little ceremony for prototype-based method sharing.
Choose factories for small counts and simplicity; constructors/classes for many instances.
How class relates
The class keyword is sugar over everything above. class Dog {} creates a constructor
function Dog, puts its methods on Dog.prototype, wires up extends via the same
prototype linking, and adds the guard that throws when called without new. There's no new
runtime mechanism — just nicer syntax over constructors and prototypes.
Key takeaways
new Fn()does four things: create an object, link it toFn.prototype, callFnwiththisbound to it, and return it (unless the constructor returns its own object).- Initialize instance state on
this; put shared methods onFn.prototype. - A constructor returning an object overrides the new instance; returning a primitive is ignored.
- Calling a constructor without
newis a classic bug —undefined/globalthis; guard withnew.target. new.targetdistinguishesnewcalls and enables abstract base constructors.- Factory functions avoid
this/newentirely at the cost of per-instance methods;classis just sugar over constructors and prototypes.
Understanding new turns constructors and classes from incantations into a clear,
four-step process you could implement yourself — which is exactly the level of understanding
that separates confident JavaScript developers from the rest.