Skip to content

When we create objects in JavaScript, we typically use the new keyword to call a function. This function called with new is what we call a constructor function. But have you ever wondered what magical changes happen to this during this process?

Let's start with a simple example:

javascript
function Person(name, age) {
  this.name = name;
  this.age = age;
  this.sayHello = function () {
    return `Hello, I'm ${this.name}`;
  };
}

const john = new Person("John", 30);
console.log(john.name); // 'John'
console.log(john.sayHello()); // "Hello, I'm John"

In this example, this no longer points to a specific object but to a newly created object. This transformation is precisely the charm of constructor functions.

The Essence of Constructor Functions

A constructor function is essentially a regular function. It becomes a constructor function entirely because of the existence of the new operator. When you use new to call a function, the JavaScript engine performs several key steps behind the scenes:

  1. Create New Object: Creates a new empty object
  2. Prototype Link: Links the new object's [[Prototype]] to the constructor function's prototype property
  3. This Binding: Points the this in the constructor function to this new object
  4. Execute Constructor: Executes the code inside the constructor function
  5. Return Object: Returns the new object if the constructor doesn't explicitly return another object
javascript
function Car(brand, model) {
  // At this point, this already points to the newly created object
  this.brand = brand;
  this.model = model;
  this.year = 2024;

  // this here is still the newly created object
  this.getInfo = function () {
    return `${this.brand} ${this.model} (${this.year})`;
  };
}

const myCar = new Car("Toyota", "Camry");
console.log(myCar.getInfo()); // "Toyota Camry (2024)"

Special Behavior of this in Constructor Functions

1. Points to the Newly Created Object

In a constructor function, this always points to the new object created by the new operator. This means you can add properties and methods to this object in the constructor function:

javascript
function Smartphone(brand, screenSize) {
  // this points to the newly created Smartphone object
  this.brand = brand;
  this.screenSize = screenSize;
  this.isTurnedOn = false;

  this.turnOn = function () {
    this.isTurnedOn = true;
    return `${this.brand} is now turned on`;
  };

  this.turnOff = function () {
    this.isTurnedOn = false;
    return `${this.brand} is now turned off`;
  };
}

const phone = new Smartphone("iPhone", "6.1 inches");
console.log(phone.turnOn()); // "iPhone is now turned on"
console.log(phone.isTurnedOn); // true

2. Nested Functions in Constructor Functions

It's important to note that this in nested functions defined within a constructor function no longer points to the newly created object. This is a common pitfall:

javascript
function Account(balance) {
  this.balance = balance;
  this.transactions = [];

  // this in this nested function doesn't point to the Account instance
  function addTransaction(amount) {
    // this here might point to window (non-strict mode) or undefined (strict mode)
    this.balance += amount; // Error!
  }

  // Correct approach:
  const self = this;
  this.addTransaction = function (amount) {
    self.balance += amount; // Use closure to preserve this
  };
}

const account = new Account(1000);
account.addTransaction(500);
console.log(account.balance); // 1500

Constructor Return Value Handling

Constructor functions have special behavior: they can handle return values differently.

1. Default Return New Object

If a constructor function doesn't have an explicit return value, or returns a primitive data type, JavaScript will return the newly created object:

javascript
function User(name) {
  this.name = name;
  // No explicit return, returns the newly created object
}

const user = new User("Alice");
console.log(user.name); // 'Alice'
javascript
function Product(name, price) {
  this.name = name;
  this.price = price;
  return 42; // Returning primitive type, ignored
}

const product = new Product("Laptop", 999);
console.log(product.price); // 999, still returns the new object

2. Explicitly Return Object

If a constructor function explicitly returns an object, that returned object will replace the newly created object:

javascript
function Singleton(name) {
  this.name = name;

  // Return an existing object
  return {
    name: "Singleton Instance",
    created: new Date(),
  };
}

const instance = new Singleton("Test");
console.log(instance.name); // 'Singleton Instance', not 'Test'
console.log(instance.created); // created property exists

This characteristic can be used to implement the singleton pattern:

javascript
function DatabaseConnection() {
  // Check if instance already exists
  if (DatabaseConnection.instance) {
    return DatabaseConnection.instance;
  }

  this.connected = false;
  this.connectionId = Math.random();

  DatabaseConnection.instance = this;
}

const db1 = new DatabaseConnection();
const db2 = new DatabaseConnection();

console.log(db1 === db2); // true
console.log(db1.connectionId === db2.connectionId); // true

Relationship with ES6 Classes

ES6 introduced class syntax, but it's essentially syntactic sugar for constructor functions:

javascript
// ES6 class syntax
class Animal {
  constructor(species, age) {
    this.species = species;
    this.age = age;
  }

  makeSound() {
    return `${this.species} makes a sound`;
  }
}

// Equivalent to constructor function syntax
function Animal(species, age) {
  this.species = species;
  this.age = age;
}

Animal.prototype.makeSound = function () {
  return `${this.species} makes a sound`;
};

In class syntax, the behavior of this in the constructor method is completely consistent with regular constructor functions:

javascript
class Book {
  constructor(title, author, price) {
    this.title = title;
    this.author = author;
    this.price = price;
    this.isAvailable = true;
  }

  purchase() {
    if (this.isAvailable) {
      this.isAvailable = false;
      return `You purchased ${this.title}`;
    }
    return `${this.title} is not available`;
  }
}

const book = new Book("JavaScript Guide", "John Doe", 29.99);
console.log(book.purchase()); // "You purchased JavaScript Guide"

Practical Application Scenarios

1. Data Model Creation

Constructor functions are ideal for creating complex data models:

javascript
function Student(id, name, grade) {
  this.id = id;
  this.name = name;
  this.grade = grade;
  this.courses = [];

  this.enrollCourse = function (courseName) {
    this.courses.push(courseName);
    return `${this.name} enrolled in ${courseName}`;
  };

  this.getAverage = function () {
    if (this.courses.length === 0) return 0;
    // Simplified average calculation
    return this.grade;
  };
}

const student = new Student(1, "Emma", 85);
student.enrollCourse("Math");
student.enrollCourse("Science");
console.log(student.courses); // ['Math', 'Science']

2. Component Creation

In front-end development, constructor functions are often used to create UI components:

javascript
function Button(text, onClick) {
  this.text = text;
  this.onClick = onClick;
  this.element = null;

  this.render = function () {
    this.element = document.createElement("button");
    this.element.textContent = this.text;
    this.element.addEventListener("click", this.onClick);
    return this.element;
  };

  this.destroy = function () {
    if (this.element) {
      this.element.removeEventListener("click", this.onClick);
      this.element.remove();
    }
  };
}

const button = new Button("Click Me", () => {
  console.log("Button clicked!");
});

document.body.appendChild(button.render());

Common Pitfalls and Solutions

1. Forgetting to Use the new Operator

javascript
function Person(name) {
  this.name = name;
}

const person1 = new Person("John"); // Correct
const person2 = Person("Jane"); // Error! this points to global object

console.log(person1.name); // 'John'
console.log(person2); // undefined
console.log(window.name); // 'Jane' (non-strict mode)

Solution: Use strict mode or add checks:

javascript
function Person(name) {
  "use strict";
  if (!(this instanceof Person)) {
    throw new Error("Constructor function must be called with new operator");
  }
  this.name = name;
}

2. Returning Wrong Object in Constructor

javascript
function BadConstructor() {
  this.property = "value";
  return { wrong: "object" }; // This will override the new object
}

const bad = new BadConstructor();
console.log(bad.property); // undefined
console.log(bad.wrong); // 'object'

Best Practices

1. Constructor Function Naming Convention

Constructor functions typically use PascalCase to distinguish them from regular functions:

javascript
function UserManager() {} // Constructor function
function getUserData() {} // Regular function

2. Add Methods to Prototype

To save memory, define methods on the prototype instead of in the constructor function:

javascript
// Not recommended: creates new function for each instance
function Circle(radius) {
  this.radius = radius;
  this.getArea = function () {
    return Math.PI * this.radius * this.radius;
  };
}

// Recommended: all instances share the same method
function Circle(radius) {
  this.radius = radius;
}

Circle.prototype.getArea = function () {
  return Math.PI * this.radius * this.radius;
};

3. Use Factory Pattern Instead of Constructor Functions

Sometimes, factory functions are more flexible than constructor functions:

javascript
// Factory function
function createVehicle(type, brand) {
  const vehicle = {
    type: type,
    brand: brand,
    drive() {
      return `${this.brand} ${this.type} is driving`;
    },
  };

  if (type === "car") {
    vehicle.doors = 4;
  } else if (type === "motorcycle") {
    vehicle.doors = 0;
  }

  return vehicle;
}

const car = createVehicle("car", "Toyota");
const motorcycle = createVehicle("motorcycle", "Honda");

Summary

this in constructor functions is the core of JavaScript's object creation mechanism. Understanding its behavior is crucial for mastering object-oriented programming:

  • New Object Binding: this points to the new object created by new
  • Return Value Handling: Returns the new object by default, explicit object return replaces the new object
  • Prototype Chain Establishment: Automatically establishes prototype links
  • Relationship with Classes: ES6 classes are syntactic sugar for constructor functions

Mastering this in constructor functions gives you mastery over JavaScript's object creation magic, enabling you to build more structured and maintainable applications.