Constructor Pattern: The Core Mechanism of JavaScript Object Creation
When building applications, we often need to create multiple objects with the same structure but containing different data—such as user lists in a system or products in a shopping cart. Manually writing object literals each time is not only inefficient but also difficult to maintain. The constructor pattern was created to solve this problem, providing a standardized way to batch create object instances.
Basic Concepts of Constructors
Constructors are special functions in JavaScript used to create objects of a specific type. By convention, constructor names usually start with a capital letter to distinguish them from regular functions.
Basic Constructor
// Basic constructor
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
// Define method in constructor (not recommended)
this.sayName = function () {
console.log(this.name);
};
}
// Create instances using the new operator
const person1 = new Person("John", 25, "Developer");
const person2 = new Person("Sarah", 30, "Designer");
console.log(person1.name); // John
console.log(person2.age); // 30
person1.sayName(); // John
person2.sayName(); // SarahHow the new Operator Works
When calling a constructor with the new operator, the JavaScript engine performs the following four steps:
- Create new object: Create a new empty JavaScript object
- Set prototype chain: Point the new object's
[[Prototype]]to the constructor'sprototype - Bind this: Point
thisin the constructor to the newly created object - Return object: If the constructor doesn't explicitly return another object, return the newly created object
function Person(name) {
// Step 3: this points to the newly created object
this.name = name;
this.sayName = function () {
console.log(this.name);
};
// Demonstrate different return scenarios
// If no explicit return, return this (newly created object)
// If returning basic type, still return this
// If returning object, return that object (not this)
}
// Step 1: Create new object {}
// Step 2: Set prototype chain: {}.__proto__ = Person.prototype
// Step 3: Bind this and execute constructor
// Step 4: Return new object
const person = new Person("John");Simulating new Operator Implementation
To better understand the new operator, we can implement a similar function ourselves:
function myNew(constructor, ...args) {
// 1. Create new object
const obj = {};
// 2. Set prototype chain
obj.__proto__ = constructor.prototype;
// 3. Bind this and execute constructor
const result = constructor.apply(obj, args);
// 4. Return result (if constructor returns object, return that object, otherwise return newly created object)
return typeof result === "object" && result !== null ? result : obj;
}
// Use our myNew function
function Person(name) {
this.name = name;
this.sayName = function () {
console.log(this.name);
};
}
const person = myNew(Person, "John");
console.log(person.name); // John
person.sayName(); // John
console.log(person.__proto__ === Person.prototype); // trueDifferences Between Constructors and Regular Functions
Differences in Calling Methods
function Person(name) {
this.name = name;
console.log("this:", this);
}
// 1. Called as constructor
const person = new Person("John");
// this: Person { name: 'John' }
// 2. Called as regular function
Person("John");
// this: Window (undefined in strict mode)Return Value Handling
function TestConstructor() {
this.prop = "created";
// Case 1: No return statement
// const obj1 = new TestConstructor();
// obj1 is { prop: 'created' }
// Case 2: Return basic type
// return 42;
// const obj2 = new TestConstructor();
// obj2 is still { prop: 'created' }
// Case 3: Return object
// return { custom: 'object' };
// const obj3 = new TestConstructor();
// obj3 is { custom: 'object' }
}
const obj = new TestConstructor();
console.log(obj.prop); // 'created'Relationship Between Prototypes and Constructors
The prototype Property
Every function has a prototype property. This property is an object containing properties and methods that can be shared by instances created by that function.
function Car(brand, model) {
this.brand = brand;
this.model = model;
}
// Define methods on prototype, shared by all instances
Car.prototype.start = function () {
console.log(`${this.brand} ${this.model} is starting`);
};
Car.prototype.stop = function () {
console.log(`${this.brand} ${this.model} is stopping`);
};
const car1 = new Car("Toyota", "Camry");
const car2 = new Car("Honda", "Civic");
car1.start(); // Toyota Camry is starting
car2.start(); // Honda Civic is starting
// Verify methods are indeed shared
console.log(car1.start === car2.start); // true
// Verify prototype chain relationship
console.log(car1.__proto__ === Car.prototype); // true
console.log(car2.__proto__ === Car.prototype); // trueThe constructor Property
By default, prototype objects have a constructor property that points back to the constructor:
function Person(name) {
this.name = name;
}
console.log(Person.prototype.constructor === Person); // true
const person = new Person("John");
console.log(person.constructor === Person); // true
console.log(person.__proto__.constructor === Person); // trueBest Practices: Define Methods on Prototypes
Method Definition Location Selection
// ❌ Not recommended: Define methods in constructor
function BadExample(name) {
this.name = name;
// Each time an instance is created, a new function object is created
this.sayName = function () {
console.log(this.name);
};
}
const person1 = new BadExample("John");
const person2 = new BadExample("Sarah");
console.log(person1.sayName === person2.sayName); // false
// Each instance has its own sayName function, wasting memory// ✅ Recommended: Define methods on prototype
function GoodExample(name) {
this.name = name;
}
GoodExample.prototype.sayName = function () {
console.log(this.name);
};
const person1 = new GoodExample("John");
const person2 = new GoodExample("Sarah");
console.log(person1.sayName === person2.sayName); // true
// All instances share the same sayName functionConstructor Template
This is a commonly used constructor template:
function Animal(species, habitat) {
// Properties unique to each instance
this.species = species;
this.habitat = habitat;
this.age = 0;
// If needed, you can define private methods here
// But it's better to use module pattern
}
// Shared methods defined on prototype
Animal.prototype.eat = function () {
console.log(`${this.species} is eating`);
this.age++;
};
Animal.prototype.sleep = function () {
console.log(`${this.species} is sleeping`);
};
Animal.prototype.getAge = function () {
return this.age;
};
// Static properties and methods (belong to constructor itself)
Animal.createHuman = function () {
return new Animal("Human", "Urban");
};
Animal.defaultDiet = "omnivore";
// Use constructor
const lion = new Animal("Lion", "Savanna");
lion.eat(); // Lion is eating
lion.sleep(); // Lion is sleeping
// Use static methods
const human = Animal.createHuman();
console.log(human.species); // Human
console.log(Animal.defaultDiet); // omnivoreConstructor Inheritance
Implementing Inheritance Using Prototype Chain
function Animal(species) {
this.species = species;
}
Animal.prototype.move = function () {
console.log(`${this.species} is moving`);
};
function Dog(name, breed) {
// Call parent constructor (inherit properties)
Animal.call(this, "Dog");
this.name = name;
this.breed = breed;
}
// Inherit prototype (inherit methods)
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog; // Fix constructor reference
// Add child-specific methods
Dog.prototype.bark = function () {
console.log(`${this.name} barks: Woof!`);
};
// Override parent method
Dog.prototype.move = function () {
console.log(`${this.name} the ${this.breed} is running`);
};
const dog = new Dog("Buddy", "Golden Retriever");
// Call child method
dog.bark(); // Buddy barks: Woof!
// Call overridden method
dog.move(); // Buddy the Golden Retriever is running
// Call parent method (through prototype chain)
Animal.prototype.move.call(dog); // Dog is movingPractical Inheritance Helper Function
function inherit(child, parent) {
// Inherit prototype
child.prototype = Object.create(parent.prototype);
// Fix constructor
child.prototype.constructor = child;
// Add reference to parent class
child.super = parent;
}
function Vehicle(wheels) {
this.wheels = wheels;
}
Vehicle.prototype.start = function () {
console.log("Vehicle started");
};
function Car(brand, model) {
Vehicle.super.call(this, 4); // Call parent constructor
this.brand = brand;
this.model = model;
}
inherit(Car, Vehicle);
Car.prototype.start = function () {
console.log(`${this.brand} ${this.model} engine started`);
};
const car = new Car("Toyota", "Camry");
car.start(); // Toyota Camry engine started
console.log(car.wheels); // 4Modular Constructor Pattern
IIFE Pattern to Protect Constructors
const PersonModule = (function () {
"use strict";
// Private variables and functions
let personCount = 0;
function validateName(name) {
return typeof name === "string" && name.length > 0;
}
// Constructor
function Person(name, age) {
if (!validateName(name)) {
throw new Error("Invalid name");
}
if (age < 0) {
throw new Error("Age cannot be negative");
}
this.name = name;
this.age = age;
this.id = ++personCount;
}
// Prototype methods
Person.prototype.sayName = function () {
console.log(`My name is ${this.name}`);
};
Person.prototype.sayAge = function () {
console.log(`I am ${this.age} years old`);
};
// Static methods
Person.getPersonCount = function () {
return personCount;
};
Person.createAdult = function (name) {
return new Person(name, 18);
};
return Person;
})();
// Use modular constructor
const person1 = new PersonModule("John", 25);
const person2 = new PersonModule("Sarah", 30);
person1.sayName(); // My name is John
person2.sayAge(); // I am 30 years old
console.log(PersonModule.getPersonCount()); // 2
const adult = PersonModule.createAdult("Mike");
console.log(adult.age); // 18Constructor Pattern Traps and Precautions
1. Forgetting to Use the new Operator
function Person(name) {
this.name = name;
}
// ❌ Forgetting to use new
const person = Person("John"); // this points to global object (non-strict mode) or undefined (strict mode)
console.log(window.name); // 'John' (pollutes global variable)
// ✅ Correctly using new
const person2 = new Person("John");
console.log(person2.name); // 'John'Solution: Self-calling Constructor
function Person(name) {
// Ensure always called with new
if (!(this instanceof Person)) {
return new Person(name);
}
this.name = name;
}
// Now both ways work
const person1 = new Person("John");
const person2 = Person("Sarah");
console.log(person1.name); // John
console.log(person2.name); // Sarah2. Prototype Accidentally Overridden
function Car(brand) {
this.brand = brand;
}
Car.prototype.drive = function () {
console.log(`${this.brand} is driving`);
};
const car = new Car("Toyota");
// ❌ Overwrite entire prototype
Car.prototype = {
fly: function () {
console.log(`${this.brand} is flying`);
},
};
console.log(car.drive); // undefined, because car's prototype didn't change
car.fly(); // TypeError: car.fly is not a function3. Reference Type Properties on Prototype
function Family() {
// Instance properties
this.familyName = "Smith";
}
// ❌ Define reference type on prototype
Family.prototype.members = []; // All instances share the same array
const family1 = new Family();
const family2 = new Family();
family1.members.push("John");
console.log(family2.members); // ['John'] - Unexpected sharing!
// ✅ Correct approach
Family.prototype.getMembers = function () {
// Each instance has its own members array
if (!this._members) {
this._members = [];
}
return this._members;
};
family1.getMembers().push("John");
console.log(family2.getMembers()); // [] - Independent arrayModern JavaScript Alternatives
Class Syntax
Modern JavaScript provides friendlier Class syntax, but underneath it's still based on constructors and prototypes:
// Modern Class syntax
class Person {
// Constructor
constructor(name, age) {
this.name = name;
this.age = age;
}
// Instance methods (equivalent to prototype methods)
sayName() {
console.log(`My name is ${this.name}`);
}
// Static methods
static createAdult(name) {
return new Person(name, 18);
}
// Getter
get info() {
return `${this.name} (${this.age} years old)`;
}
}
// Use Class
const person = new Person("John", 25);
person.sayName(); // My name is John
console.log(person.info); // John (25 years old)
const adult = Person.createAdult("Sarah");
console.log(adult.age); // 18Factory Function Pattern
Sometimes, factory functions are more flexible than constructors:
function createPerson(name, age) {
return {
name,
age,
sayName() {
console.log(`My name is ${this.name}`);
},
sayAge() {
console.log(`I am ${this.age} years old`);
},
};
}
const person = createPerson("John", 25);
person.sayName(); // My name is John
// Advantages: No need to use new, can return different object structures
// Disadvantages: No explicit type checking, each instance has its own method copySummary
The constructor pattern is the fundamental mechanism for creating objects in JavaScript, and understanding it is crucial for mastering JavaScript object-oriented programming:
- Constructors are special functions used to create objects of specific types
- The new operator is responsible for creating objects, setting prototypes, binding this, and returning objects
- Methods should be defined on prototypes to achieve memory sharing and performance optimization
- Inheritance requires combining prototype chains and constructor calls to implement complete property and method inheritance
- Watch out for common pitfalls like forgetting new, prototype overwriting, and reference type sharing
Although modern JavaScript provides Class syntax and other friendlier APIs, the underlying mechanism is still based on constructors and prototypes. Understanding how constructors work helps us better understand JavaScript's object-oriented features and write more efficient, robust code.