Constructor Functions
Before the class keyword arrived in ES2015, JavaScript built reusable object “types” with constructor functions — ordinary functions invoked with the new keyword. They are still everywhere: in legacy code, in libraries, and under the hood of every class you write. Understanding them demystifies what new actually does and shows you the real machinery that classes only dress up.
What a constructor function is
A constructor function is just a regular function intended to be called with new. By convention its name is capitalized (User, Point) to signal that intent. Inside the function, this refers to the brand-new object being built, and you attach the per-instance data to it.
function User(name, email) {
this.name = name;
this.email = email;
this.active = true;
}
const alice = new User("Alice", "[email protected]");
const bob = new User("Bob", "[email protected]");
console.log(alice.name);
console.log(bob.email);
console.log(alice.active);
Output:
Alice
[email protected]
true
Each call to new User(...) produces a fresh, independent object. There is no explicit return — new takes care of returning the constructed object for you.
What new actually does
The new keyword is the real workhorse. When you write new User("Alice", ...), the engine performs four steps:
- Creates a brand-new empty object.
- Links that object’s
[[Prototype]]toUser.prototype. - Binds
thisto the new object and runs the function body. - Returns the new object automatically — unless the function explicitly returns its own (non-primitive) object.
function User(name) {
// 1 & 2 happen before this body runs:
// this = a new object whose prototype is User.prototype
this.name = name; // 3: populate `this`
// 4: `return this` is implicit
}
const u = new User("Carol");
console.log(u);
console.log(Object.getPrototypeOf(u) === User.prototype);
Output:
User { name: 'Carol' }
true
Forgetting
newis a classic bug.User("Dave")(nonew) runs the function withthisundefined in strict mode, throwing aTypeError; in sloppy modethisleaks to the global object and silently corrupts global state. Always call constructors withnew.
Attaching methods to the prototype
If you put methods inside the constructor body, every instance gets its own copy of that function — wasteful when you create thousands of objects. The right place for shared methods is the constructor’s prototype object. Because every instance links to User.prototype, they all share a single function reference via the prototype chain.
function User(name, email) {
this.name = name;
this.email = email;
}
// Shared by every instance — defined once.
User.prototype.greet = function () {
return `Hi, I'm ${this.name}`;
};
User.prototype.describe = function () {
return `${this.name} <${this.email}>`;
};
const alice = new User("Alice", "[email protected]");
const bob = new User("Bob", "[email protected]");
console.log(alice.greet());
console.log(bob.describe());
console.log(alice.greet === bob.greet); // same function reference
Output:
Hi, I'm Alice
Bob <[email protected]>
true
The this inside greet refers to whichever instance the method was called on, even though the function itself lives on the prototype. Keep per-instance data on this in the constructor; keep behavior on the prototype.
| Where you define it | Lives on | Memory cost | Use for |
|---|---|---|---|
this.x = ... in constructor | The instance | One copy per instance | Per-instance data |
Fn.prototype.method = ... | The prototype | One copy total, shared | Shared behavior |
The instanceof check
instanceof tells you whether an object was built from a particular constructor. It works by walking the object’s prototype chain and checking whether the constructor’s .prototype appears anywhere along it — so it respects inheritance, not just the direct constructor.
function User(name) {
this.name = name;
}
const alice = new User("Alice");
console.log(alice instanceof User);
console.log(alice instanceof Object); // Object.prototype is on the chain
console.log(alice instanceof Array);
console.log([] instanceof Array);
Output:
true
true
false
true
Because instanceof inspects the prototype chain rather than a stored tag, reassigning Fn.prototype after objects exist can make older instances report false. For most code, though, it is the idiomatic way to ask “what kind of object is this?”
Bridge to classes
The ES2015 class syntax is syntactic sugar over exactly this pattern. A class’s constructor body is the constructor function, and methods declared in the class body land on Class.prototype automatically. The two snippets below are functionally equivalent.
// Constructor-function style
function User(name) {
this.name = name;
}
User.prototype.greet = function () {
return `Hi, I'm ${this.name}`;
};
// class style — same prototypes underneath
class UserClass {
constructor(name) {
this.name = name;
}
greet() {
return `Hi, I'm ${this.name}`;
}
}
const a = new User("Eve");
const b = new UserClass("Eve");
console.log(a.greet());
console.log(b.greet());
console.log(typeof UserClass); // a class is still a function
Output:
Hi, I'm Eve
Hi, I'm Eve
function
Classes add real conveniences — they enforce new, support extends/super cleanly, and keep methods non-enumerable — but the underlying model is the prototype mechanism you just saw. Reach for class in new code; understand constructor functions so the magic stops being magic.
Best Practices
- Capitalize constructor names (
User, notuser) so callers know to usenew. - Always invoke constructors with
new; in strict mode a missingnewthrows, which is a feature, not a bug. - Put per-instance state on
thisand shared methods onFn.prototypeto avoid duplicating functions across instances. - Use
instanceofto test type membership, but remember it walks the prototype chain and can be fooled by prototype reassignment. - Prefer regular
functiondeclarations for constructors — arrow functions have nothisbinding and cannot be called withnew. - For new projects, prefer the
classsyntax; it compiles down to this exact pattern but is clearer and enforces correct usage.