You're writing JavaScript and suddenly realize with horror and dread that you've written a huge function. It's happened to the best of us. Now comes the almost unstoppable urge to separate the function in smaller pieces.
Big functions mean a lot of responsibilities in one place and, possibly, code smell too, so it's a good idea in itself. But you want to break up the function in such a way that it doesn't lose its meaning or context. To do that, you need to think method chaining - also called function chaining. This article will explain what I mean by that.
Prerequisites
Before we get started, here are a few JavaScript concepts that you already need to be familiar with:
Most experienced developers will have some experience with Array
methods, such as map, reduce, and filter. You've probably already come across something like this:
const food = [
{ name: "Banana", type: "fruit" },
{ name: "Apple", type: "fruit" },
{ name: "Chocolate", type: "candy" },
{ name: "Orange", type: "fruit" }
];
// This type of usage is very common
food
.map(item => item.type)
.reduce((result, fruit) => {
result.push(fruit);
return [...new Set(result)];
}, []);
// result: ['fruit', 'candy']
Method chaining happens when you want to call multiple functions using the same object and its reference. In the above example, the array method map
returns an Array
, which has a formidable number of methods.
Because you return a reference pointing to an Array
, you'll have access to all the properties of Array
. It's the same principle that you use for your own method chaining.
Better Understanding this
I won't cover all the nuances of this
, but it's good to understand why this
functions the way it does. No matter how experienced you are, the keyword this can be hard to understand. It's often the reason why things don't work the way you expect them to 😅.
Here's the gist of it: this
will always point to the current scope
or instance from where it's called. An example:
const dog = {
is: null,
log: () => console.log(this.is),
bark() {
this.is = "woofing";
this.log();
return this;
},
walk() {
this.is = "walking";
this.log();
return this;
},
eat() {
this.is = "eating";
this.log();
return this;
}
};
dog
.bark()
.eat()
.walk();
When you write a function like this (no pun intended), you might want to chain methods that are related to the object. The object dog
has 3 methods: walk
, bark
and eat
. Every time I call any of its functions, a console should show the representation of what the dog is doing at that exact moment.
But there's a problem with that. If you run it in the browser, you'll realize it doesn't work as expected. That's because arrow functions use lexical scoping, where this
refers to its surrounding scope - in this case window
, not the object itself.
To resolve that, we should use an anonymous function call to represent the log
property:
// instead of this:
log: () => console.log(this.is),
// use this:
log() {
console.log(this.is);
}
Now the dog's this
scope is going to be accessible inside the function.
Using Class
You can achieve the same result using classes:
class Dog {
is = null;
log() {
console.log(this.is);
}
bark() {
this.is = "woofing";
this.log();
return this;
}
walk() {
this.is = "walking";
this.log();
return this;
}
eat() {
this.is = "eating";
this.log();
return this;
}
}
const dog = new Dog();
dog
.bark()
.eat()
.walk();
Using Prototype
If you have to use prototype, do it this way:
function Dog() {}
Dog.prototype.is = null;
Dog.prototype.log = function() {
console.log(this.is);
};
Dog.prototype.bark = function() {
this.is = "woofing";
this.log();
return this;
};
Dog.prototype.walk = function() {
this.is = "walking";
this.log();
return this;
};
Dog.prototype.eat = function() {
this.is = "eating";
this.log();
return this;
};
const dog = new Dog();
dog
.bark()
.eat()
.walk();
What About Async Functions?
async
functions are synthetic sugar for promises and generators. When you declare an async
function, you know that it will return a promise. Because of that, you'll also have access to all the methods of the promise.
const requests = {
user: null,
action: null,
log(something) {
console.log(this[something]);
},
async getUser() {
this.user = await new Promise(resolve => {
setTimeout(() => {
resolve("Douglas Pires");
}, 1000);
});
this.log("user");
return this;
},
async registerAction() {
this.action = await new Promise(resolve => {
setTimeout(() => {
resolve("programming stuff");
}, 1000);
});
this.log("action");
return this;
}
};
requests.getUser().then(() => requests.registerAction());
However, it's not a good idea to chain a bunch of promises together. I recommend using a bunch of await
keywords instead. It's easier and it will make your code much more readable.
await requests.getUser(); // Douglas Pires
await requests.registerAction(); // programming
And you'll have access to the same properties here:
console.log(requests.user); // Douglas Pires
console.log(requests.action); // programming
In Conclusion
Chaining functions comes in handy when you have to manipulate an object. It's also a practical way to improve the readability of your code. However, as you've seen above, you should chain wisely.
In this article, we've talked about this
, classes, prototypes, and async
functions in the context of JavaScript method chaining. I hope you've picked up some useful tips and are now better informed about the topic. You can find my repository with all code on JavaScript method chaining right here. See you in the next post!
Enjoyed reading this article? Feel free to share on social media using the buttons below. Alternatively, I've also written about how you can build a color palette generator using Nuxt and Vuetify. Go check it out!