Object Factories

What is an Object Factory?

In JavaScript, object factories let you create multiple instances of similar objects without defining a specific type. This approach can be handy as a simplified way to generate objects.

An object factory is simply a function that returns a new object. Each time the function is called, it creates a new instance of the object, perhaps configured with different properties based on the arguments passed to it.

Assume we want to create several objects that represent cats. We might start by just creating several literal objects:

let cocoa = {
  name: 'Cocoa',
  color: 'black',
  age: 5,

  speak() {
    console.log(
      `Meow. I am ${this.name}. ` +
      `I am a ${this.age}-year-old ${this.color} cat.`
    );
  },
}

let leo = {
  name: 'Leo',
  color: 'orange',
  age: 3,

  speak() {
    console.log(
      `Meow. I am ${this.name}. ` +
      `I am a ${this.age}-year-old ${this.color} cat.`
    );
  },
}

Even with just two objects, you can see a lot of repetition with these object literals.

Consider the following code:

function createCat(name, color, age) {
  return {
    name: name,
    color: color,
    age: age,

    speak() {
      console.log(
        `Meow. I am ${this.name}. ` +
        `I am a ${this.age}-year-old ${this.color} cat.`
      );
    }
  };
}

let cocoa = createCat("Cocoa", "black", 5);
let leo = createCat("Leo", "orange", 3);

cocoa.speak();
// Meow. I am Cocoa. I am a 5-year-old black cat.

leo.speak();
// Meow. I am Leo. I am a 3-year-old orange cat.

In this code, createCat is a factory function. It simply returns an object that, in this case, represents a cat with the name, color, and age provided by the arguments. When we call it on lines 16 and 17, we get two different instance objects that represent cats: one for Cocoa and one for Leo. We can see that the two objects are different by calling the speak method.

We can use concise (or compact) property syntax to define the values for name, color, and age. If a property name matches that of the variable used to initialize it, we can omit the : and the variable name. Thus, we can rewrite our code like this:

function createCat(name, color, age) {
  return {
    //highlight
    name,
    color,
    age,
    //endhighlight

    speak() {
      console.log(
        `Meow. I am ${this.name}. ` +
        `I am a ${this.age}-year-old ${this.color} cat.`
      );
    }
  };
}

We will use this concise property syntax throughout this book.

Advantages and Disadvantages of Object Factories

Object factories in JavaScript provide a simple, functional approach to object creation. They have several advantages and disadvantages.

Advantages

Object factories let programmers create highly customized objects with different properties and methods based on the arguments passed to the factory function. That can be particularly useful for applications that need to generate objects dynamically.

For many scenarios, primarily when inheritance isn't needed, object factories can provide a more straightforward way to create objects without dealing with the complexities of prototype chains (which we'll discuss later).

Object factories also bypass one of the trickiest JavaScript concepts you will encounter: execution context and the associated this keyword. Instead of dealing with the this keyword, you write a function that returns an object. Everything your object needs will typically be built-in to the object, with no dependencies on the execution context. We'll discuss execution contexts and this in more detail later.

Disadvantages

Each object created by a factory function gets copies of the methods defined by the returned object. For instance, in the createCat example earlier, all cats will have their own copy of the speak method. For this reason, factory functions can use high amounts of memory when creating many objects.

Implementing inheritance using object factories can be more cumbersome and less performance-efficient than the more advanced approaches. While achieving inheritance-like behaviors is technically possible, it can involve manually copying methods from one object type to another.

The final issue with object factories is that the objects they create don't have a "type". When using a constructor, you can use the instanceof operator or the constructor property to determine the type of an object.

function Foo() {
  this.foo = 42;
}

let obj = new Foo();
console.log(obj instanceof Foo); // true
console.log(obj.constructor);    // [Function: Foo]

However, this does not work as expected with objects created by an object factory:

function Foo() {
  return {
    foo: 42,
  };
}

let obj = Foo();
console.log(obj instanceof Foo); // false
console.log(obj.constructor);    // [Function: Object]

When Should You Use Factory Functions?

Object factories are most useful in the following scenarios:

  • When you do not need inheritance.
  • When your application already uses object factories.
  • When you aren't concerned about memory usage or only expect to use a small number of objects.
  • When you only have a few simple types in your code. In such cases, you're unlikely to have problems if you need to inspect objects while debugging.
  • When the specific type of an object is unimportant to your application.

Try to avoid using object factories in these scenarios:

  • When inheritance is required.
  • When you need many objects with methods.
  • When the types of your objects are essential to your application.

Summary

Object factories offer a functional alternative to more complex object creation approaches such as constructor functions and classes. They promote flexibility and encapsulation while avoiding common JavaScript pitfalls, such as using the this keyword. However, there may be better choices for scenarios that require extensive object-oriented designs with inheritance chains or applications where memory usage is a critical concern. Object factories are an excellent tool in specific contexts. Still, they require careful consideration to balance their benefits with their drawbacks.

Exercises

  1. Given the following three objects, create an object factory that can eliminate the code duplication:

    let apple = {
      name: 'Apple',
      color: 'Red',
    
      isRipe: function() {
        return `This ${this.name} is ripe.`;
      },
    
      describe: function() {
        return `This ${this.name} is ${this.color}.`;
      },
    };
    
    let banana = {
      name: 'Banana',
      color: 'Yellow',
    
      isRipe: function() {
        return `This ${this.name} is ripe.`;
      },
    
      describe: function() {
        return `This ${this.name} is ${this.color}.`;
      },
    };
    
    let blackberry = {
      name: 'Blackberry',
      color: 'Black',
    
      isRipe: function() {
        return `This ${this.name} is ripe.`;
      },
    
      describe: function() {
        return `This ${this.name} is ${this.color}.`;
      },
    };
    

    Solution

    function createFruit(name, color) {
      return {
        name: name,
        color: color,
    
        isRipe: function() {
          return `This ${this.name} is ripe.`;
        },
    
        describe: function() {
          return `This ${this.name} is ${this.color}.`;
        },
      };
    }
    
    let apple = createFruit('Apple', 'Red');
    let banana = createFruit('Banana', 'Yellow');
    let blackberry = createFruit('Blackberry', 'Black');
    
  2. This exercise re-examines exercise 4 from the previous chapter. In that exercise, you wrote a constructor function to instantiate smartphone objects. In this exercise, we'll rewrite that constructor as an object factory.

    Write a factory function that creates objects that represent smartphones. Each smartphone should have a brand, model, and release year. Add methods to check the battery level and to display the smartphones's information. Create objects that represent the following two smartphones:

    Brand Model Release Year
    Apple iPhone 12 2020
    Samsung Galaxy S21 2021

    Solution

    function Smartphone(brand, model, releaseYear) {
      return {
        brand,
        model,
        releaseYear,
    
        checkBatteryLevel: function() {
          return `${this.brand} ${this.model} has 75% battery remaining.`;
        },
    
        displayInfo: function() {
          return `${this.releaseYear} ${this.brand} ${this.model}`;
        },
      };
    }
    
    let iphone12 = new Smartphone('Apple', 'iPhone 12', 2020);
    let galaxyS21 = new Smartphone('Samsung', 'Galaxy S21', 2021);
    
    console.log(iphone12.checkBatteryLevel());
    // Apple iPhone 12 has 75% battery remaining.
    
    console.log(iphone12.displayInfo());
    // 2020 Apple iPhone 12
    
    console.log(galaxyS21.checkBatteryLevel());
    // Samsung Galaxy S21 has 75% battery remaining.
    
    console.log(galaxyS21.displayInfo());
    // 2021 Samsung Galaxy S21
    
  3. Create an object factory that returns objects representing musical instruments. Each instrument should have a name and a type. Users of these objects should be able to play the instrument and show its type. Use the factory function to create three instruments:

    • A cello is a string instrument.
    • A flute is a wind instrument.
    • A drum is a percussion instrument.
    let violin = createInstrument('violin', 'string');
    violin.play();     // We are playing a tune on this violin
    violin.showType(); // This violin is a string instrument
    
    let flute = createInstrument('flute', 'wind');
    flute.play();      // We are playing a tune on this flute
    flute.showType();  // This flute is a wind instrument
    
    let drum = createInstrument('drum', 'percussion');
    drum.play();       // We are playing a tune on this drum
    drum.showType();   // This drum is a percussion instrument
    

    Solution

    function createInstrument(name, type) {
      return {
        name: name,
        type: type,
    
        play() {
          console.log(`We are playing a tune on this ${this.name}.`);
        },
    
        showType() {
          console.log(`This ${this.name} is a ${this.type} instrument.`);
        }
      };
    }