Learn JS Series (#37) - Constructor Functions and the new Operator, Step by Step
Learn JS Series (#37) - Constructor Functions and the new Operator, Step by Step
What will I learn
- You will learn what a constructor function is, and the naming convention that signals one;
- exactly what the
newoperator does, in four precise steps; - what the
prototypeproperty on a function is, and how it becomes an instance's prototype; - how to put shared methods on the constructor's prototype;
- what goes wrong if you forget
new, and how to reason about it.
Requirements
- A working modern computer running macOS, Windows or Ubuntu;
- An installed Node.js (20+) distribution, or just a modern browser console;
- Episodes 1-36 read, especially prototypes and Object.create.
Difficulty
- Intermediate
Curriculum (of the Learn JS Series):
- Learn JS Series (#1) - What Is JavaScript, Why It Runs Everywhere, and How to Run It
- Learn JS Series (#2) - Variables and Bindings
- Learn JS Series (#3) - The Primitive Types: number, string, boolean, null, undefined, symbol, bigint
- Learn JS Series (#4) - Operators and Expressions: Arithmetic, Comparison, Logical, and Short-Circuiting
- Learn JS Series (#5) - Strings: Template Literals, Unicode, and the Methods You Actually Use
- Learn JS Series (#6) - Numbers: IEEE 754, Why 0.1 + 0.2 Is Not 0.3, and How to Cope
- Learn JS Series (#7) - Control Flow: if/else, switch, and the Ternary Expression
- Learn JS Series (#8) - Loops: for, while, for...of, for...in, and When to Use Which
- Learn JS Series (#9) - Functions: Declarations, Parameters, Return Values, and Hoisting
- Learn JS Series (#10) - Scope and the Temporal Dead Zone: How JavaScript Finds Your Variables
- Learn JS Series (#11) - Arrays: The Workhorse Data Structure and Its Core Methods
- Learn JS Series (#12) - Objects: Key-Value Data, Dot vs Bracket Access, and Nesting
- Learn JS Series (#13) - Truthiness, Equality, and Coercion: == vs === Done Properly
- Learn JS Series (#14) - Mini Project: A Command-Line Tip Calculator
- Learn JS Series (#15) - First-Class Functions: Passing, Returning, and Storing Functions
- Learn JS Series (#16) - Arrow Functions vs function: Syntax, this, and When Each Wins
- Learn JS Series (#17) - Closures: The Single Most Important Idea in JavaScript
- Learn JS Series (#18) - Higher-Order Functions: Functions That Take or Return Functions
- Learn JS Series (#19) - Callbacks and the Callback Pattern (Before We Reach Promises)
- Learn JS Series (#20) - Default, Rest, and Spread: Flexible Function Signatures
- Learn JS Series (#21) - Destructuring Parameters: Named Arguments the JS Way
- Learn JS Series (#22) - The this Keyword: Five Rules That Explain Every Case
- Learn JS Series (#23) - call, apply, and bind: Controlling this Explicitly
- Learn JS Series (#24) - Recursion: Base Cases, the Call Stack, and Stack Overflows
- Learn JS Series (#25) - IIFEs and the Module Pattern (the Pre-2015 Way to Get Privacy)
- Learn JS Series (#26) - Currying and Partial Application
- Learn JS Series (#27) - Function Composition: Building Pipelines from Small Functions
- Learn JS Series (#28) - Pure Functions and Side Effects: The Foundation of Predictable Code
- Learn JS Series (#29) - Memoization: Trading Memory for Speed with Closures
- Learn JS Series (#30) - Mini Project: A Small Functional Utility Library
- Learn JS Series (#31) - Object Literals, Shorthand, and Computed Property Names
- Learn JS Series (#32) - Property Descriptors: writable, enumerable, configurable
- Learn JS Series (#33) - Getters and Setters: Computed Properties That Look Like Data
- Learn JS Series (#34) - The Prototype: JavaScript's Actual Inheritance Mechanism
- Learn JS Series (#35) - The Prototype Chain: How Property Lookup Really Works
- Learn JS Series (#36) - Object.create and Pure Prototypal Inheritance
- Learn JS Series (#37) - Constructor Functions and the new Operator, Step by Step (this post)
Learn JS Series (#37) - Constructor Functions and the new Operator, Step by Step
Last episode we built inheritance with our bare hands, wiring one object to another with Object.create and nothing else. That was the modern honest road to prototypal inheritance. Today we walk the older road, the one that JavaScript shipped with back in 1995 and that every codebase from that era is built on: constructor functions and the new operator. And here is the reassuring part -- it leads to exactly the same destination. Same prototype chain, same shared methods, same per-object data. Only the spelling changes.
Why bother with the old road at all, when class is right around the corner? Because class is sugar over this, not over Object.create. When you finally write class in the next episode, every line of it will map onto a constructor function and a prototype object -- the very two things we take apart today. So think of this episode as the last piece of scaffolding before the pretty facade goes up. Having said that, let's build it properly. ;-)
Solutions to Episode 36 Exercises
Exercise 1 - a shape and a circle:
const shape = {
init(name) { this.name = name; return this; },
area() { return 0; },
};
const circle = Object.create(shape);
circle.area = function () { return Math.PI * this.radius ** 2; };
const c = Object.create(circle).init("disc");
c.radius = 2;
console.log(c.name, c.area().toFixed(2)); // "disc" "12.57"
The insight: area is overridden on circle, while name still comes from shape's inherited init.
Exercise 2 - override that calls the parent:
const person = { greet() { return "hello"; } };
const employee = Object.create(person);
employee.greet = function () {
return `${person.greet.call(this)}, I am a ${this.title}`;
};
const e = Object.create(employee);
e.title = "engineer";
console.log(e.greet()); // "hello, I am a engineer"
The insight: person.greet.call(this) runs the inherited version with the current this, then we extend it, exactly what super will do.
Exercise 3 - create with descriptors:
const proto = { hi() { return `hi ${this.name}`; } };
const obj = Object.create(proto, {
name: { value: "scipio", enumerable: true },
secret: { value: 1, enumerable: false },
});
console.log(Object.keys(obj), obj.hi(), obj.secret); // ["name"] "hi scipio" 1
The insight: one Object.create call both linked the prototype and defined own properties with chosen flags.
Now the older, function-based road to that same place, and the operator that makes it work.
Constructor functions: a convention, not a keyword
Before Object.create and long before class, JavaScript's original way to stamp out many similar objects was the constructor function. Here is the thing that trips people up, so I will say it plainly: a constructor function is just an ordinary function. There is no special constructor keyword, no distinct kind of function object, nothing the parser treats differently. What makes a function "a constructor" is purely how you call it -- with new in front -- plus a naming convention the whole community agreed on.
That convention is: a constructor's name is capitalized (PascalCase), precisely to signal "call me with new":
function User(name, level) {
this.name = name; // 'this' is the new object (thanks to new)
this.level = level;
this.active = true;
}
const alice = new User("alice", 5);
const bob = new User("bob", 3);
console.log(alice); // User { name: 'alice', level: 5, active: true }
console.log(bob); // User { name: 'bob', level: 3, active: true }
Called with new, User builds a fresh object, assigns properties to it through this, and hands it back. Two short lines produce two completely independent objects -- alice and bob share no state, each has its own name, level, and active. Notice also that the console prints User { ... } and not just { ... }: the engine remembers which constructor made the object, a small detail we will cash in later when we talk about instanceof.
But look at how much is happening implicitly. We never wrote const obj = {}. We never wrote return obj. Yet alice is a fully formed object. Where did it come from, how did this become it, and who returned it? That is the new operator doing four quiet jobs on your behalf, and it is genuinely worth knowing each one.
What new does, in four steps
When you evaluate new User("alice", 5), JavaScript performs four precise steps. Learn these four and the mystery evaporates completely:
- Create a brand new empty object.
- Link that new object's prototype to
User.prototype(the function'sprototypeproperty, explained in the next section). - Call
Userwiththisbound to the new object, running its body (which sets properties onthis). - Return the new object automatically -- unless the constructor explicitly returns an object of its own, in which case that object wins.
The best way to convince yourself there is no magic here is to rebuild new out of tools you already own. Everything below uses only Object.create (episode 36) and apply (episode 23):
function User(name) {
this.name = name;
}
// what `new User("scipio")` effectively does, by hand:
function manualNew(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 ? result : obj; // step 4
}
const a = new User("scipio");
const b = manualNew(User, "scipio");
console.log(a.name, b.name); // "scipio" "scipio" - identical behaviour
Trace it line by line. Object.create(Constructor.prototype) creates the empty object and links its prototype in one go -- that is steps 1 and 2 fused, which is exactly why episode 36 was such useful groundwork. Constructor.apply(obj, args) calls the function with this set to our new object and the arguments spread in -- step 3. And the final line is step 4: if the constructor happened to return its own object we hand that back, otherwise we hand back the object we built. new is nothing more than this sequence, wrapped up in a single keyword. You have already seen every ingredient.
That step-4 subtlety is worth a quick demonstration, because it catches people out. If a constructor returns a primitive (a number, a string, undefined), that return value is ignored and you still get the new object. But if it returns an object, that object replaces the one new would have given you:
function Weird() {
this.a = 1;
return { b: 2 }; // returning an object overrides the default
}
function Fine() {
this.a = 1;
return 42; // returning a primitive is ignored
}
console.log(new Weird()); // { b: 2 } - our object was thrown away!
console.log(new Fine()); // Fine { a: 1 } - the 42 was ignored
You will almost never want to return an object from a constructor (it defeats the point), but knowing the rule means you will never be baffled by it either.
The prototype property
Step 2 mentioned User.prototype, and this is the single most confusing word in the whole language, so let us nail it down hard. Every regular function -- yes, every one, whether you ever use it as a constructor or not -- automatically comes with a property literally called prototype, and that property holds an object. When you use the function with new, the newly created instance's internal prototype link is pointed at that prototype object. In other words, the function's prototype property becomes the shared prototype of all the instances it creates:
function Dog(name) {
this.name = name;
}
const rex = new Dog("Rex");
// the instance's prototype IS Dog.prototype:
console.log(Object.getPrototypeOf(rex) === Dog.prototype); // true
You must hold two different meanings of the word "prototype" apart, or this will forever feel slippery. Dog.prototype is a property sitting on the function object -- think of it as the blueprint that instances will point at. Object.getPrototypeOf(rex) is the instance's actual live prototype link -- the thing property lookup climbs. The new operator is the bridge between them: it makes the second point at the first. Dog.prototype is the blueprint; rex's hidden link is the wire; new solders the wire to the blueprint.
There is one more strand on that prototype object, and it closes a nice little loop. By default, Dog.prototype has a property called constructor that points right back at Dog. Because rex inherits from Dog.prototype, rex.constructor finds it too:
console.log(Dog.prototype.constructor === Dog); // true
console.log(rex.constructor === Dog); // true - inherited from Dog.prototype
console.log(rex.constructor.name); // "Dog"
So an object can, in principle, tell you which constructor built it. Do not lean on this too heavily (it can be reassigned, and inheritance can break it if you are careless), but it is genuinely handy for debugging and it explains that Dog { ... } label the console printed earlier.
Shared methods on the prototype
Here is the real payoff of the whole model, and the reason constructor-plus-prototype beat "just put everything in the constructor". If you assign methods inside the constructor body, like this.speak = function () {...}, then every single instance gets its own private copy of that function. Ten instances, ten identical copies of speak; ten thousand instances, ten thousand copies. That is wasteful, and pointless, because the method never changes per object.
The fix is to put shared behaviour on the constructor's prototype object exactly once, so all instances inherit one shared copy through the prototype chain:
function Dog(name) {
this.name = name; // per-instance data goes on 'this'
}
// shared behaviour goes on the prototype: ONE copy for ALL instances:
Dog.prototype.speak = function () {
return `${this.name} says woof`;
};
Dog.prototype.legs = 4;
const rex = new Dog("Rex");
const fido = new Dog("Fido");
console.log(rex.speak()); // "Rex says woof"
console.log(fido.speak()); // "Fido says woof"
console.log(rex.speak === fido.speak); // true - the SAME shared function
Study that last line, because it proves the whole point. rex and fido each carry their own name, stamped on by the constructor -- but rex.speak and fido.speak are the identical function object, living up on Dog.prototype. The === returns true because there is only one speak in memory, inherited by both, not copied into either. When you call rex.speak(), the engine does not find speak on rex, hops one link up to Dog.prototype, finds it, and runs it with this bound to rex -- which is why this.name reads "Rex". This is precisely the memory-efficient sharing we saw in episode 34, now driven by a constructor instead of a hand-rolled Object.create.
The rule to carry with you forever: per-instance data goes on this inside the constructor; shared behaviour goes on the prototype. Data down here, behaviour up there. Break that rule and you either waste memory (methods on this) or accidentally share mutable state between all instances (mutable data on the prototype, a classic beginner trap -- put an array on Dog.prototype and every dog shares the same array).
Forgetting new
Because a constructor really is just a function, JavaScript will happily let you call it without new -- and the result is a quiet little disaster. Remember the four steps? Without new, none of them happen. No new object is created, this is not bound to a fresh object (it is undefined in strict mode, or the global object in sloppy mode), and the function's return value is used as-is -- which is undefined, since our constructor has no return:
"use strict";
function User(name) {
this.name = name;
}
const good = new User("scipio"); // correct: an object
console.log(good.name); // "scipio"
// const bad = User("scipio"); // ERROR (runtime): Cannot set properties of undefined
// without new, 'this' is undefined in strict mode, so this.name throws
In strict mode this throws immediately, which is actually the kind outcome -- you find the bug at once. In old sloppy-mode code it was far nastier: this became the global object, so this.name = name silently created a global variable name, your bad variable got undefined, and nothing complained until something far away broke. This "oops, forgot new" footgun was common enough that it directly motivated the class syntax we meet next episode: a class simply cannot be called without new -- it throws a clear TypeError the instant you try -- removing the trap entirely.
Until then, the capitalization convention is your manual guard. PascalCase names (User, Dog, Point) are a signal to every reader, including future-you at midnight, that this function must be called with new. It is not enforced by the engine, it is enforced by discipline -- but it is a discipline worth keeping.
A quick look sideways: the same idea in Python
Quite a few of you arrived here from the Learn Python Series, so the comparison is genuinely clarifying -- it shows JavaScript's constructor is one answer among several to a universal need: set up a new object and give it shared behaviour.
In Python, __init__ is doing the exact job of our constructor body (stamping per-instance data onto self), and the class block itself is what holds the shared methods -- the equivalent of our prototype object:
class Dog:
legs = 4 # class attribute: shared, like Dog.prototype.legs
def __init__(self, name):
self.name = name # per-instance data, like this.name
def speak(self):
return f"{self.name} says woof"
rex = Dog("Rex")
fido = Dog("Fido")
print(rex.speak()) # "Rex says woof"
print(rex.speak == fido.speak) # methods resolved on the class, not copied
The mapping is almost one-to-one: __init__ is the constructor body, self is this, class attributes are prototype properties, and Dog("Rex") (no new needed in Python) is the whole new Dog("Rex") dance. The philosophical difference is that Python bakes a formal class object into the language from the start, whereas JavaScript hands you a plain function plus a plain prototype object and lets you wire them -- the same wiring we did by hand with manualNew. Seeing that both languages solve the same problem with different clothing is exactly what makes JavaScript's class (next episode) feel like a natural convergence rather than a random bolt-on.
Try it yourself
- Write a constructor function
Book(title, author)that setsthis.titleandthis.author. Add a shared methodsummary()onBook.prototypereturning"title by author". Create two books and confirm they share the samesummaryfunction with===. - Implement your own
manualNew(Constructor, ...args)(as in the episode) and use it to create an instance of yourBook. Verify it behaves identically to usingnew, and confirm the instance's prototype isBook.prototypeand thatinstance.constructorpoints atBook. - Write a constructor and call it once with
newand once without (in strict mode). Describe what happens in each case, and explain in one sentence which of the fournewsteps are skipped when you forget it.
So what did we actually cover?
- A constructor function is an ordinary function meant to be called with
new, conventionally named in PascalCase -- the capitalization is the only "constructor-ness" there is. newdoes four steps: create an empty object, link its prototype to the function'sprototypeproperty, call the function withthisas that object, and return it (unless the body returns its own object).- You can replicate
newby hand withObject.createandapply, proving it is not magic -- just a packaged sequence. - A function's
prototypeproperty becomes the shared prototype of every instance created withnew;Object.getPrototypeOf(instance) === Constructor.prototype, andprototype.constructorpoints back at the function. - Put per-instance data on
thisand shared methods onConstructor.prototype, so all instances inherit one copy (memory-efficient), and never put mutable data on the prototype by accident. - Forgetting
newbreaks everything (badthis, no object returned); PascalCase guards against it by convention, andclassremoves the footgun entirely.
Next episode we finally reach the class keyword, and you will see it is pure syntactic sugar over exactly the constructor-and-prototype machinery we just assembled by hand.