How to Use Constructor Functions and the "new" Operator in JavaScript
Before ES6 classes were introduced, constructor functions were the primary way to create multiple objects with the same structure and behavior in JavaScript. Even today, understanding constructor functions is essential because classes are built on top of them, and you will encounter the constructor pattern in countless existing codebases, libraries, and documentation.
A constructor function is a regular function that, when called with the new keyword, creates a new object, sets up its properties, and returns it. The new operator is the mechanism that transforms an ordinary function call into an object-creation process.
This guide explains how constructor functions work, what happens behind the scenes when you use new, how to add methods, what happens when a constructor returns a value, and how the pattern compares to factory functions.
What Is a Constructor Function?
A constructor function is technically just a regular function with two conventions:
- Its name starts with a capital letter (PascalCase)
- It is intended to be called with the
newoperator
function User(name, age) {
this.name = name;
this.age = age;
this.isActive = true;
}
let alice = new User("Alice", 30);
let bob = new User("Bob", 25);
console.log(alice);
// User { name: "Alice", age: 30, isActive: true }
console.log(bob);
// User { name: "Bob", age: 25, isActive: true }
console.log(alice.name); // "Alice"
console.log(bob.age); // 25
Each call to new User(...) creates a brand-new, independent object. The function acts as a blueprint or template, and new is the mechanism that produces the actual instances.
The Capital Letter Convention
The capital letter naming convention is not enforced by the language. JavaScript does not care whether a function name starts with an uppercase or lowercase letter. However, this convention is universally followed in the JavaScript community to signal that a function is meant to be used with new.
// Convention: PascalCase for constructors
function Car(make, model) {
this.make = make;
this.model = model;
}
// Convention: camelCase for regular functions
function calculateTax(amount) {
return amount * 0.2;
}
When you see new Car(...), the capital C immediately tells you that Car is a constructor. When you see calculateTax(...), the lowercase c tells you it is a regular function.
Always follow the naming convention. Use PascalCase for constructor functions and camelCase for regular functions. This makes your code immediately readable and prevents confusion about whether new is required.
The "new" Operator: What Happens Under the Hood
When you call a function with new, JavaScript performs a specific sequence of steps behind the scenes. Understanding these steps demystifies the entire constructor mechanism.
The Four Steps of "new"
let alice = new User("Alice", 30);
Here is what happens internally when this line executes:
Step 1: A new empty object is created and assigned to this.
this = {}; // (internally, not valid syntax. Just for illustrating the concept)
Step 2: The new object's internal [[Prototype]] is set to User.prototype.
this.__proto__ = User.prototype; // (simplified representation)
Step 3: The function body executes with this referring to the new object. Properties are added to this.
this.name = "Alice"; // adds property to the new object
this.age = 30; // adds property to the new object
this.isActive = true; // adds property to the new object
Step 4: The new object (this) is automatically returned (unless the function explicitly returns a different object).
return this; // (implicit, happens automatically)
Visualizing the Process
To make this completely concrete, here is the equivalent of what new User("Alice", 30) does, written as plain code:
function User(name, age) {
this.name = name;
this.age = age;
this.isActive = true;
}
// What "new User('Alice', 30)" does conceptually:
function simulateNew() {
const obj = {}; // Step 1: Create empty object
Object.setPrototypeOf(obj, User.prototype); // Step 2: Set prototype
User.call(obj, "Alice", 30); // Step 3: Execute function with obj as this
return obj; // Step 4: Return the object
}
let alice = simulateNew();
console.log(alice); // { name: "Alice", age: 30, isActive: true }
This is not the exact internal implementation, but it accurately represents the logic. The key insight is that new creates the object for you and sets up this to point to it.
What Happens Without "new"?
If you call a constructor function without new, it behaves as a regular function. Since there is no new object created, this follows the normal rules: it will be undefined in strict mode or the global object in non-strict mode.
function User(name) {
this.name = name;
}
// ❌ WRONG: Calling without new in non-strict mode
let alice = User("Alice"); // No new!
console.log(alice); // undefined (the function returns nothing)
console.log(window.name); // "Alice" (accidentally set on the global object!)
In strict mode, this throws an error instead of silently polluting the global scope:
"use strict";
function User(name) {
this.name = name;
}
let alice = User("Alice");
// TypeError: Cannot set properties of undefined (setting 'name')
new with Constructor FunctionsForgetting new is a common and dangerous mistake. In non-strict mode, it silently creates global variables. In strict mode, it throws an error. Always ensure constructor functions are called with new.
Calling "new" with Any Function
Technically, any function can be called with new. JavaScript does not restrict new to functions that "look like" constructors:
function greet() {
console.log("Hello!");
}
let obj = new greet(); // Creates an empty object, prints "Hello!"
console.log(obj); // greet {} (an empty object with greet.prototype)
This works, but it is meaningless and confusing. The naming convention (PascalCase) exists precisely to avoid this confusion.
Return from Constructors
Normally, constructor functions do not have an explicit return statement. The new object (this) is returned automatically. However, if you do include a return statement, the behavior depends on what you return.
Returning an Object: Overrides this
If a constructor explicitly returns an object, that object is returned instead of this:
function User(name) {
this.name = name;
// Returning a different object (overrides the default behavior)
return { name: "Override!", role: "admin" };
}
let user = new User("Alice");
console.log(user.name); // "Override!" (the returned object won)
console.log(user.role); // "admin"
Output:
Override!
admin
The properties set on this (like this.name = "Alice") are discarded because the explicitly returned object takes precedence.
Returning a Primitive: Ignored
If a constructor returns a primitive value (string, number, boolean, etc.), the return is completely ignored, and this is returned as normal:
function User(name) {
this.name = name;
return 42; // Primitive (ignored by new)
}
let user = new User("Alice");
console.log(user.name); // "Alice" (this was returned, not 42)
console.log(user); // User { name: "Alice" }
Output:
Alice
User { name: "Alice" }
Return Rules Summary
| Return Statement | What new Returns |
|---|---|
No return statement | this (the new object) |
return with a primitive (number, string, boolean, null, undefined) | this (primitive is ignored) |
return with an object ({}, array, function, etc.) | The returned object (overrides this) |
In practice, constructor functions almost never have an explicit return statement. The automatic return of this is the expected behavior. If you find yourself returning an object from a constructor, consider whether a factory function might be a clearer pattern.
Omitting Parentheses
If a constructor takes no arguments, the parentheses can be omitted (though this is not recommended for readability):
function User() {
this.name = "Default";
}
let user = new User; // Works (parentheses are optional with no arguments)
let user2 = new User(); // Also works (preferred for clarity)
console.log(user.name); // "Default"
console.log(user2.name); // "Default"
Always include the parentheses, even when there are no arguments, to maintain consistency and clarity.
Methods in Constructors
Constructor functions can add methods (functions) to the newly created object, just like they add data properties:
function User(name, age) {
// Data properties
this.name = name;
this.age = age;
// Method
this.introduce = function() {
console.log(`Hi, I'm ${this.name} and I'm ${this.age} years old.`);
};
// Method with logic
this.isAdult = function() {
return this.age >= 18;
};
}
let alice = new User("Alice", 30);
let charlie = new User("Charlie", 15);
alice.introduce(); // "Hi, I'm Alice and I'm 30 years old."
charlie.introduce(); // "Hi, I'm Charlie and I'm 15 years old."
console.log(alice.isAdult()); // true
console.log(charlie.isAdult()); // false
A More Complex Example
function Calculator() {
this.history = [];
this.add = function(a, b) {
const result = a + b;
this.history.push(`${a} + ${b} = ${result}`);
return result;
};
this.subtract = function(a, b) {
const result = a - b;
this.history.push(`${a} - ${b} = ${result}`);
return result;
};
this.getHistory = function() {
return this.history.join("\n");
};
}
let calc = new Calculator();
calc.add(5, 3); // 8
calc.subtract(10, 4); // 6
calc.add(1, 1); // 2
console.log(calc.getHistory());
// "5 + 3 = 8"
// "10 - 4 = 6"
// "1 + 1 = 2"
Each instance of Calculator gets its own history array and its own set of methods, making instances fully independent.
The Memory Cost of Methods in Constructors
There is an important caveat. When you define methods inside a constructor using this.method = function() {...}, every instance gets its own copy of that function:
function User(name) {
this.name = name;
this.greet = function() {
console.log(`Hi, I'm ${this.name}`);
};
}
let alice = new User("Alice");
let bob = new User("Bob");
// Each instance has its OWN greet function
console.log(alice.greet === bob.greet); // false (two separate function objects)
Output:
false
If you create 1,000 User objects, you get 1,000 copies of the greet function in memory. For data properties like name, this is expected and correct (each user has their own name). But methods that behave the same for all instances are being duplicated unnecessarily.
The solution to this memory inefficiency is using prototypes, where a single copy of the method is shared across all instances. This is covered in the prototypes module. ES6 classes solve this automatically by placing methods on the prototype.
// Preview: The prototype-based approach (covered in detail later)
function User(name) {
this.name = name;
}
User.prototype.greet = function() {
console.log(`Hi, I'm ${this.name}`);
};
let alice = new User("Alice");
let bob = new User("Bob");
console.log(alice.greet === bob.greet); // true (same function, shared via prototype)
Defining methods inside the constructor (this.method = ...) creates a new function for each instance. This is fine for small numbers of objects, but for many instances, placing methods on the prototype is more memory-efficient. ES6 classes handle this automatically.
The Constructor Pattern vs. Factory Functions
Constructor functions are not the only way to create objects with shared structure. Factory functions are regular functions that return a new object, without using new or this.
Factory Function Example
function createUser(name, age) {
return {
name,
age,
isActive: true,
introduce() {
console.log(`Hi, I'm ${name} and I'm ${age} years old.`);
}
};
}
let alice = createUser("Alice", 30);
let bob = createUser("Bob", 25);
alice.introduce(); // "Hi, I'm Alice and I'm 30 years old."
bob.introduce(); // "Hi, I'm Bob and I'm 25 years old."
Constructor Function Equivalent
function User(name, age) {
this.name = name;
this.age = age;
this.isActive = true;
this.introduce = function() {
console.log(`Hi, I'm ${this.name} and I'm ${this.age} years old.`);
};
}
let alice = new User("Alice", 30);
let bob = new User("Bob", 25);
alice.introduce(); // "Hi, I'm Alice and I'm 30 years old."
bob.introduce(); // "Hi, I'm Bob and I'm 25 years old."
Comparison
| Feature | Constructor Function | Factory Function |
|---|---|---|
Called with new | Yes | No |
Uses this | Yes | No |
instanceof works | Yes (alice instanceof User) | No |
| Prototype chain | Automatic (via User.prototype) | No (returns plain objects) |
Forgetting new | Bugs or errors | No issue (no new needed) |
| Private variables | Not directly (without closures) | Yes (via closure over local variables) |
| Convention | PascalCase (User) | camelCase (createUser) |
Factory Functions Can Have Private Variables
One advantage of factory functions is that local variables inside them are naturally private, accessible only through the returned methods (closures):
function createCounter(initialValue = 0) {
let count = initialValue; // Private (not accessible from outside)
return {
increment() {
count++;
},
decrement() {
count--;
},
getCount() {
return count;
}
};
}
let counter = createCounter(10);
counter.increment();
counter.increment();
counter.decrement();
console.log(counter.getCount()); // 11
console.log(counter.count); // undefined (count is private)
Output:
11
undefined
The variable count is only accessible through the methods. There is no way to access or modify it directly from outside.
When to Use Which?
Use constructor functions (or classes) when:
- You need
instanceofchecks - You want to use prototypal inheritance
- You are following an existing codebase's conventions
- You are building a class hierarchy
Use factory functions when:
- You want simplicity (no
new, nothisissues) - You need true private state without the
#syntax - You want to avoid the risk of forgetting
new - You are composing behavior from multiple sources (mixins)
In modern JavaScript, ES6 classes have largely replaced constructor functions for most use cases. However, constructor functions remain the foundation upon which classes are built, and understanding them deeply gives you a complete picture of how JavaScript object creation works.
new.target: Detecting "new" Calls
JavaScript provides a special meta-property called new.target that allows a function to detect whether it was called with new or as a regular function.
Basic Usage
Inside a function, new.target is:
- A reference to the function itself if called with
new undefinedif called withoutnew
function User(name) {
console.log("new.target:", new.target);
this.name = name;
}
new User("Alice");
// new.target: [Function: User]
// Calling without new (in non-strict mode):
// User("Alice");
// new.target: undefined
Preventing Calls Without "new"
You can use new.target to enforce that a constructor is always called with new:
function User(name) {
if (!new.target) {
throw new Error("User must be called with 'new'!");
}
this.name = name;
}
let alice = new User("Alice"); // Works fine
console.log(alice.name); // "Alice"
// let bob = User("Bob"); // Error: User must be called with 'new'!
Making a Function Work Both Ways
Some libraries allow their constructors to be called with or without new, automatically adding new if it was forgotten:
function User(name) {
if (!new.target) {
// Called without new (redirect to a new call)
return new User(name);
}
this.name = name;
}
// Both work identically:
let alice = new User("Alice");
let bob = User("Bob"); // Automatically wrapped in new
console.log(alice.name); // "Alice"
console.log(bob.name); // "Bob"
console.log(bob instanceof User); // true
This pattern is sometimes called a "self-healing constructor". It makes the API more forgiving, but it can also hide mistakes. Whether to use this pattern depends on your library's design philosophy.
The self-healing constructor pattern can mask developer errors. In most application code, it is better to throw an error when new is forgotten, forcing developers to fix the call site. The self-healing pattern is more appropriate for public library APIs where usability is prioritized.
new.target in ES6 Classes
new.target also works in class constructors and is particularly useful for detecting which class was instantiated in an inheritance chain:
class Animal {
constructor() {
console.log("new.target:", new.target.name);
}
}
class Dog extends Animal {
constructor() {
super();
}
}
new Animal(); // new.target: Animal
new Dog(); // new.target: Dog
Even though Dog's constructor calls super() which runs Animal's constructor, new.target still refers to Dog, the class that was actually instantiated. This enables patterns like abstract base classes that refuse direct instantiation:
class Shape {
constructor() {
if (new.target === Shape) {
throw new Error("Shape is abstract and cannot be instantiated directly");
}
}
}
class Circle extends Shape {
constructor(radius) {
super();
this.radius = radius;
}
}
let circle = new Circle(5); // Works fine
// let shape = new Shape(); // Error: Shape is abstract...
Practical Example: Building a Task Manager
Let's put everything together with a practical example that demonstrates constructor functions, methods, and instance independence:
function Task(title, priority) {
if (!new.target) {
throw new Error("Task must be called with 'new'");
}
this.id = Task._nextId++;
this.title = title;
this.priority = priority || "medium";
this.completed = false;
this.createdAt = new Date();
this.complete = function() {
this.completed = true;
console.log(`Task "${this.title}" completed!`);
};
this.describe = function() {
const status = this.completed ? "Done" : "Pending";
console.log(
`[#${this.id}] ${this.title} | Priority: ${this.priority} | Status: ${status}`
);
};
}
// Static property for auto-incrementing IDs
Task._nextId = 1;
// Create instances
let task1 = new Task("Learn JavaScript", "high");
let task2 = new Task("Buy groceries");
let task3 = new Task("Read a book", "low");
task1.describe();
// [#1] Learn JavaScript | Priority: high | Status: Pending
task2.describe();
// [#2] Buy groceries | Priority: medium | Status: Pending
task1.complete();
// Task "Learn JavaScript" completed!
task1.describe();
// [#1] Learn JavaScript | Priority: high | Status: Done
// Each instance is independent
console.log(task2.completed); // false (unaffected by task1.complete())
This example shows:
new.targetto prevent calling withoutnew- Auto-incrementing IDs using a static property on the constructor
- Default parameter values (
priority || "medium") - Methods that operate on instance data via
this - Instance independence: completing
task1does not affecttask2
Summary
- A constructor function is a regular function designed to be called with
new. By convention, its name uses PascalCase. - The
newoperator performs four steps: creates an empty object, sets its prototype, executes the function withthispointing to the new object, and returns the object. - If a constructor explicitly returns an object, that object replaces
this. If it returns a primitive, the return is ignored andthisis returned as normal. - Methods can be defined inside constructors using
this.method = function() {...}, but each instance gets its own copy. For shared methods, prototypes (or classes) are more efficient. - Factory functions are an alternative pattern that returns plain objects without
neworthis. They offer simplicity and natural privacy but lackinstanceofand prototype chain features. new.targetis a meta-property that tells you whether a function was called withnew. Use it to enforce correct usage or to create self-healing constructors.- In modern JavaScript, ES6 classes are the preferred syntax for the constructor pattern, but they are built on top of constructor functions and prototypes. Understanding constructors gives you deep insight into how classes work under the hood.