DEV Community

Cover image for Factory arrow functions in JavaScript
Sergei Orlov
Sergei Orlov

Posted on • Updated on • Originally published at orlow.dev

Factory arrow functions in JavaScript

This article describes an alternative approach to instantiating objects from a template in JavaScript. For better comprehension, it is sometimes compared with commonly used ES6 classes.

DISCLAIMER
The term factory arrow function is made up as I couldn't succeed in googling for a proper name for that. I avoid calling it factory function because it is an existing term that means a different thing. If you know the correct name for the concept covered in this article, please, share it in a comment - I'd be glad to find out.

It's a Series

ES6 Class Recap

If you do not know what ES6 classes in JavaScript are, I suggest to read the official MDN article about classes but full understanding and experience with classes is not a required pre-requisite for this article. Here is a short recap:

Classes are a template for creating objects. They encapsulate data with code to work on that data. Classes in JS are built on prototypes but also have some syntax and semantics that are not shared with ES5 classalike semantics. © MDN
MDN

Key features of ES6 classes:

  • Familiar syntax for developers coming from other programming languages
  • They don't hoist, no matter if they are used as class expressions or class declarations
  • In methods declared on a class, this represents the current object instantiated from the class
  • The body of the class always operates in strict mode
  • Subclassing is possible using the extends keyword, referencing parent class is possible using the super keyword
  • Instance can be checked for being an instanceof a constructor (beware, dragons here)
  • The new keyword is used to instantiate a class

You've most probably seen classes in JavaScript as they have become a common part of our codebases these days. Here is an example of an ES6 class declaration:

class Rectangle {
    constructor(length, width) {
        this.length = length
        this.width = width
    }

    getArea() {
        return this.length * this.width
    }
}

const r = new Rectangle(10, 20)
r.getArea() // 200
Enter fullscreen mode Exit fullscreen mode

Factory arrow function

Although classes have many benefits, I found myself using a different approach that I would like to share here. In JavaScript, we can create a function that accepts arguments and returns an object that has exclusive access to those arguments via closure.

Here is an example:

const rectangle = (length, width) => ({
    length,
    width,
    getArea: () => length * width,
})

const r = rectangle(10, 20)
r.getArea() // 200
Enter fullscreen mode Exit fullscreen mode

This example uses a few shortcuts, so it's ok if it seems unfamiliar. Here is what it would look like if we wrote it in a more traditional way:

const rectangle = (length, width) => {
    return {
        length,
        width,
        getArea: () => length * width,
    }
}
Enter fullscreen mode Exit fullscreen mode

Now I'd like to outline the cool features this approach gives us compared to ES6 class syntax.

no this

As we use arrow functions both for the methods and for object creation, this is undefined. JavaScript this requires a solid understanding of its behaviour and using it can be misleading for many developers. Instead of relying on this, we can benefit from using the closure over the arguments. As the object has access to the arguments, it means that they are available in its methods.

We also enable safe method extraction because of the closure.

const rectangle = (length, width) => ({
    width,
    length,
    getArea: () => length * width,
})

const theRectangle = rectangle(10, 20)

const getTheRectangleArea = theRectangle.getArea
getTheRectangleArea() // 200
Enter fullscreen mode Exit fullscreen mode

NOTE: We can achieve safe method extraction with classes, for example using Function.prototype.bind, but with the factory arrow function, we no longer need to bother about losing the context.

Private properties

It is impossible to directly change the arguments passed to a function from the outside. They cannot be accessed, and they cannot be changed. You can explicitly allow access by binding the arguments to object properties. In the example below, length is available on the object externally, but width only exists inside and there is no way to access it from outside the object:

const rectangle = (length, width) => ({
    length,
    getArea: () => length * width,
})

const r = rectangle(10, 20)
r.length // 10
r.width // undefined
r.getArea() // 200
Enter fullscreen mode Exit fullscreen mode

Free Bonus: even if you assign different values on the accessible object properties, the object itself will still use the arguments in its methods. Keep in mind that it only works if you don't use the properties of the object from the outside.

const rectangle = (length, width) => ({
    length,
    width,
    getTotalAreaWith: ({ length: oLength, width: oWidth }) => length * width + oLength * oWidth, // <- This is the cause
})

const r1 = rectangle(2, 5)
const r2 = rectangle(3, 6)

r1.getTotalAreaWith(r2) // 28

r1.width = 1000
r1.getTotalAreaWith(r2) // 28

r2.width = 1000
r1.getTotalAreaWith(r2) // 3010 <- This is the problem
Enter fullscreen mode Exit fullscreen mode

You can avoid the issue with accidental overrides of object property values by doing all the calculations internally in the object:

const rectangle = (length, width) => ({
    length,
    width,
    getArea: () => length * width,
    getTotalAreaWith: ({ getArea }) => length * width + getArea(), // <- Now it will work
})

const r1 = rectangle(2, 5)
const r2 = rectangle(3, 6)

r1.getTotalAreaWith(r2) // 28

r1.width = 1000
r1.getTotalAreaWith(r2) // 28

r2.width = 1000
r1.getTotalAreaWith(r2) // 28
Enter fullscreen mode Exit fullscreen mode

No Direct Inheritance And Internal Method Calls

If you looked at the previous example, you probably noticed that length is multiplied by width in two places: in getArea and in getTotalAreaWith. This is because we cannot use this and access getArea from inside getTotalAreaWith, which is a good example that everything has a price.

The factory arrow function also does not allow us to use inheritance which may cause code repetition as well.

But, due to the anonymous nature of our methods, we can write those separately and build up a horizontal extension of our objects and share methods between or even outside the objects.

A simple way to do so is to use partial application.

If you are not familiar with the concept of partial application, I suggest you read Kyle Shevlin's article on the topic.

In the example below, I create a multiplyThunk that is partially applied with two values. I then assign it as a getArea method on multiple different factory arrow function return objects and make it work for multiple shapes with a single function:

const multiplyThunk = (a, b) => () => a * b

const rectangle = (length, width) => ({
    length,
    width,
    getArea: multiplyThunk(length, width),
})

const square = (length) => ({
    length,
    getArea: multiplyThunk(length, length),
})

const circle = (radius) => ({
    radius,
    getArea: multiplyThunk(Math.PI, radius ** 2),
})
Enter fullscreen mode Exit fullscreen mode

NOTE: Using the partial application is possible in ES6 classes, but there is a small chance you would need to do it as you would generally prefer to use this and extends.

Composition over Inheritance

Although inheritance is not available to us with factory arrow functions, we can choose composition over inheritance, which means that we can extend from multiple objects at once. This way, we can create lightweight objects with the methods and properties we really need in a specific situation.

NOTE: This is also possible with ES6 classes. This approach is called Mix-in.

const squarePerimeter = (length) => ({
    getPerimeter: () => 4 * length,
})

const squareArea = (length) => ({
    getArea: () => length ** 2,
})

const LengthyShape = (...features) => (length) => ({
    length,
    ...features.reduce(
        (acc, feature) => ({
            ...acc,
            ...feature(length),
        }),
        {},
    ),
})

const squareWithPerimeter = LengthyShape(squarePerimeter)
const square = LengthyShape(squarePerimeter, squareArea)

const sp = squareWithPerimeter(5)
sp.getArea() // Uncaught TypeError: sp.getArea() is not a function
sp.getPerimeter() // 20

const s = square(5)
s.getArea() // 25
s.getPerimeter() // 20
Enter fullscreen mode Exit fullscreen mode

Static Methods

For the sake of convenience, you can imitate static methods. Static methods are methods on a class that can be called without instantiating the class itself. They are also non-callable when the class is instantiated, i.e. you cannot refer to them via this on the instance. Static methods are commonly used for utility functions in our app but they have other areas of application as well.

With factory arrow functions, we can declare properties on the functions themselves to obey both laws of static methods. We can declare static properties the same way.

const Square = (length) => ({
    length,
    getArea: () => length ** 2,
})

Square.new = Square

const s = Square.new(10) // <- Looks like Rust!
s.getArea() // 100
Enter fullscreen mode Exit fullscreen mode

Conclusion

This article covered using factory arrow functions in JavaScript. In the next one, I extend the topic by covering factory arrow function usage with TypeScript.

I hope you enjoyed the read!

Top comments (0)