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.
Object factories in JavaScript provide a simple, functional approach to object creation. They have several advantages and disadvantages.
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.
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]
Object factories are most useful in the following scenarios:
Try to avoid using object factories in these scenarios:
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.
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}.`;
},
};
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');
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 |
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
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:
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
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.`);
}
};
}