Skip to main content

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:

  1. Its name starts with a capital letter (PascalCase)
  2. It is intended to be called with the new operator
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.

tip

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')
Always Use new with Constructor Functions

Forgetting 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 StatementWhat new Returns
No return statementthis (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)
info

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)
Methods in Constructors vs. 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

FeatureConstructor FunctionFactory Function
Called with newYesNo
Uses thisYesNo
instanceof worksYes (alice instanceof User)No
Prototype chainAutomatic (via User.prototype)No (returns plain objects)
Forgetting newBugs or errorsNo issue (no new needed)
Private variablesNot directly (without closures)Yes (via closure over local variables)
ConventionPascalCase (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 instanceof checks
  • 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, no this issues)
  • 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
  • undefined if called without new
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.

Use new.target Carefully

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.target to prevent calling without new
  • 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 task1 does not affect task2

Summary

  • A constructor function is a regular function designed to be called with new. By convention, its name uses PascalCase.
  • The new operator performs four steps: creates an empty object, sets its prototype, executes the function with this pointing 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 and this is 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 new or this. They offer simplicity and natural privacy but lack instanceof and prototype chain features.
  • new.target is a meta-property that tells you whether a function was called with new. 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.