What Method Chaining in JavaScript Is, How It Works and How to Use It
Method chaining is popular method that can help you write more concise and readable code. In this tutorial, you will learn what method chaining in JavaScript is and how it works. You will also learn how to use method chaining to improve the quality and readability of your code.
A quick introduction to method chaining in JavaScript
Have you ever worked with some library such as jQuery? Then, you’ve probably seen something like this. There are two or more methods used in cascade, one after another, and on the same line. Nowadays, it is also very common to see this practice in plain JavaScript. You could see this with arrays, strings and promises.
In all these cases, the process is the same. First, you reference the thing you want to work with. Second, you use as many methods as you need. However, instead of using these methods separately, you use them one after another. You basically chain them together. Let’s take a look at some examples to demonstrate this.
Examples of method chaining
Let’s say you want to work with a string. There are two ways to get this done. The first one is without method chaining. This requires using each method on the string separately. You also have to reference the string each and every time. The second option is to use method chaining.
In this case, you use all string methods you want one after another. You can do this either on a single line or multiple. That depends on you. And, you also reference the string only once, at the very beginning. The same result, but different amount of code that you have to write.
// Method chaining with string. let myStr = ' - Hello-world. ' // Without method chaining: myStr = myStr.toLowerCase() myStr = myStr.replace(/-/g, ' ') myStr = myStr.trim() // With method chaining: myStr = myStr.toLowerCase().replace(/-/g, ' ').trim() // Alternative with method chaining and multiple lines: myStr = myStr .toLowerCase() .replace(/-/g, ' ') .trim() // Log the value of "myStr" variable. console.log(myStr) // Output: // 'hello world.'
The same applies if you have an array and want to use a couple of array methods to work with it. You can also choose between these two approaches. The longer one not using method chaining and the shorter and more succinct using chaining. Just like with the string, the result will be the same. The amount code will be different.
// Method chaining with array. let myArray = [1, 7, 3, null, 8, null, 0, null, '20', 15] // Without method chaining: myArray = myArray.filter(el => typeof el === 'number' && isFinite(el)) myArray = myArray.sort((x, y) => x - y) // With method chaining: myArray = myArray.filter(el => typeof el === 'number' && isFinite(el)).sort((x, y) => x - y) // Alternative with method chaining and multiple lines: myArray = myArray .filter(el => typeof el === 'number' && isFinite(el)) .sort((x, y) => x - y) // Log the value of "myArray" variable. console.log(myArray) // Output: // [ 0, 1, 3, 7, 8 ]
Promises are a very good example because they almost require method chaining in order to work. First, you [create a promise]. Then, you add appropriate handler functions. These handler functions are necessary for you to process the value you get when the promise is resolved. Well, unless you use async function and await keyword.
// Create a Promise const myPromise = new Promise((resolve, reject) => { // Create a fake delay setTimeout(function() { // Resolve the promise with a simple message resolve('Sorry, no data.') }, 1000) }) // With method chaining: myPromise.then((data) => console.log(data)).catch(err => console.log(error)) // Alternative with method chaining and multiple lines: myPromise .then((data) => console.log(data)) .catch(err => console.log(error)) // Output: // 'Sorry, no data.'
How method chaining in JavaScript works
You know how method chaining looks like. The more important question is, how it works. The answer is very simple. It works because of this. Yes, we are talking about the notorious this keyword. When it comes to this there is a lot one can learn. To keep this tutorial short, let’s not get too deep and keep it simple instead.
Let’s say you have an object. If you use this inside that object it will refer to that object. If you then create an instance, or copy, of that object, this will refer to that instance or copy. When you work with some string or array method you are working with an object.
const myObj = { name: 'Stuart', age: 65, sayHi() { // This here refers to myObj return `Hi my name is ${this.name}.` }, logMe() { console.log(this) } } myObj.sayHi() // Output: // 'Hi my name is Stuart.' myObj.logMe() // Output: // { // name: 'Stuart', // age: 65, // sayHi: ?, // logMe: ? // }
In case of a string you are working with a primitive data type. However, the method you are using, such as toLowerCase(), exists on the prototype of a String object. Having a new method on some object is not enough to make chaining work. There is one critical ingredient, the this.
For chaining to work, a method must return the object it works with. It has to return the this. Think about this as a baton. There are some runners on the field in different positions. However, they can’t all run at once. Only one can run at the time. When currently running runner completes his part, he has to pass the baton to the next runner.
Only when this happens, when the next runner receives the baton, can he run his part. In our case, each method is a runner. The baton is returned this, the object the method is working with. If there is not baton, no this returned, next runner can’t run and chaining will not work.
How to implement method chaining in JavaScript
That was about the theory. Now, to practice. So, in order to make chaining work you need three things. First, you need some object. Second, that object needs some methods you can later call. Third, these methods have to return the object itself. They have to return the this if you want to make them chainable.
Let’s create a simple object as a metaphor for a person. This person will have few properties: name, age and state. The state will specify in what state the person currently is. To change this state, there will be few methods: walk(), sleep(), eat(), drink(), work() and exercise().
Since we want all these method to be chainable they all have to return this in the very end. There will be also one utility method. This method will log the current state to console. When you use one of the methods to change person’s state, it will also call this method will so you can see the new state in console.
// Create person object. const person = { name: 'Jack Doer', age: 41, state: null, logState() { console.log(this.state) }, drink() { // Change person's state. this.state = 'Drinking.' // Log current person's state. this.logState() // Return this to make the method chainable. return this }, eat() { // Change person's state. this.state = 'Eating.' // Log current person's state. this.logState() // Return this to make the method chainable. return this }, exercise() { // Change person's state. this.state = 'Exercising.' // Log current person's state. this.logState() // Return this to make the method chainable. return this }, sleep() { // Change person's state. this.state = 'Sleeping.' // Log current person's state. this.logState() // Return this to make the method chainable. return this }, walk() { // Change person's state. this.state = 'Walking.' // Log current person's state. this.logState() // Return this to make the method chainable. return this }, work() { // Change person's state. this.state = 'Working.' // Log current person's state. this.logState() // Return this to make the method chainable. return this } } // Let's have some fun. person .drink() // Output: 'Drinking.' .exercise() // Output: 'Exercising.' .eat() // Output: 'Eating.' .work() // Output: 'Working.' .walk() // Output: 'Walking.' .sleep() // Output: 'Sleeping.' // Alternative on a single line: person.drink().exercise().eat().work().walk().sleep() // Output: // 'Drinking.' // 'Exercising.' // 'Eating.' // 'Working.' // 'Walking.' // 'Sleeping.'
Methods, chaining, this and arrow functions
The necessity to work with this also means one thing. You can’t create chainable methods with arrow functions. The reason is that, in arrow functions, this is not bound to the instance of object. this will refer to the global object window. If you try to return this it will return window, not the object itself.
Another issue would be accessing and changing object properties from the inside of the arrow function. Since this would be global object window you could not use it to reference the object and then its property. You would be trying to reference window and its property.
There is a way to bypass this, if you insist on using arrow functions. Instead of using this to reference the object you would have to reference the object by its name directly. You would have to replace all occurrences of this inside arrow functions with the object name.
// Create person object. const person = { name: 'Jack Doer', age: 41, state: null, logState() { console.log(this.state) }, drink: () => { person.state = 'Drinking.' person.logState() return person }, eat: () => { person.state = 'Eating.' person.logState() return person }, exercise: () => { person.state = 'Exercising.' person.logState() return person }, sleep: () => { person.state = 'Sleeping.' person.logState() return person }, walk: () => { person.state = 'Walking.' person.logState() return person }, work: () => { person.state = 'Working.' person.logState() return person } } // Let's have some fun. person .drink() // Output: 'Drinking.' .exercise() // Output: 'Exercising.' .eat() // Output: 'Eating.' .work() // Output: 'Working.' .walk() // Output: 'Walking.' .sleep() // Output: 'Sleeping.'
One potential downside of this is that you would also loose all flexibility. If you copy the object, all arrow functions will still be hard-wired to the original object. This will happen if you create the copy with both, Object.assign() and Object.create().
// Create original person object. const person = { name: 'Jack Doer', age: 41, state: null, logState() { // Log the whole object. console.log(this) }, drink: () => { person.state = 'Drinking.' person.logState() return person }, eat: () => { person.state = 'Eating.' person.logState() return person } } // Let person eat. person.eat() // Output: // { // name: 'Jack Doer', // age: 41, // state: 'Eating.', // logState: ?, // drink: ?, // eat: ? // } // Create new object based on person object. const newPerson = new Object(person) // Change the "name" and "age" properties. newPerson.name = 'Jackie Holmes' newPerson.age = 33 // Let newPerson drink. // This will print Jack Doer not Jackie Holmes. newPerson.drink() // Output: // { // name: 'Jack Doer', // age: 41, // state: 'Drinking.', // logState: ?, // drink: ?, // eat: ? // }
However, the problem above will not happen if you use Object() constructor. If you use the Object() constructor, with new keyword, you will create that new object as an independent copy. When you use some method on that copy it will have effect only on that copy, not the original.
// Create original person object. const person = { name: 'Jack Doer', age: 41, state: null, logState() { // Log the whole object. console.log(this) }, drink: () => { person.state = 'Drinking.' person.logState() return person }, eat: () => { person.state = 'Eating.' person.logState() return person } } // Let person eat. person.eat() // Output: // { // name: 'Jack Doer', // age: 41, // state: 'Eating.', // logState: ?, // drink: ?, // eat: ? // } // Create new object based on person object. const newPerson = new Object(person) // Change the "name" and "age" properties. newPerson.name = 'Jackie Holmes' newPerson.age = 33 // Let newPerson drink. newPerson.drink() // Output: // { // name: 'Jackie Holmes', // age: 33, // state: 'Drinking.', // logState: ?, // drink: ?, // eat: ? // }
So, if you insist on using arrow functions, and want to copy objects? It will be better to create those copies with Object() constructor and new keyword. Otherwise, spare yourself the hustle and just use regular functions.
Method chaining and classes
Are a fan of JavaScript classes? Then, I have some good news for you. You can use method chaining in JavaScript also if you prefer to work with classes. The process is the same as with object, only the syntax is a bit different. The important thing is that every method that should be chainable must return this.
// Create Person class. class Person { constructor(name, age) { this.name = name this.age = age this.state = null } logState() { console.log(this.state) } drink() { this.state = 'Drinking.' this.logState() return this } eat() { this.state = 'Eating.' this.logState() return this } sleep() { this.state = 'Sleeping.' this.logState() return this } } // Create instance of Person class. const joe = new Person('Joe', 55) // Use method chaining. joe .drink() // Output: 'Drinking.' .eat() // Output: 'Eating.' .sleep() // Output: 'Sleeping.'
Conclusion: What method chaining in JavaScript is, how it works and how to use it
Method chaining is one simple method that can be quite useful. It can help you write code that is shorter and more readable. I hope that this tutorial helped you understand what method chaining in JavaScript is and how it works. Now, it is up to you to use what you’ve learned about method chaining in your code.
If you liked this article, please subscribe so you don't miss any future post.
Automation QA Engineer
6 个月Thanks for this article! Very clear explanation ??
Software Engineer
7 个月In the arrow functions section, you used Object()?constructor in both the second and third code examples. I believe you meant to use Object.assign()?or?Object.create() for the second example.