This Is Why JavaScript Method Chaining Is So Useful
Let's look at a technique called Method Chaining, aka Function Chaining, and find out how it can make our code more concise and readable.
For the examples in this article, we'll work with JavaScript's class syntax in conjunction with the Web Audio API.
(The Web Audio API is native to JavaScript, available in the browser, and allows the creation and manipulation of sound sources.)
Let's Write Some Code!
Let's start by setting up our index.html file:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Web Audio API</title>
</head>
<body>
<script src="app.js"></script>
</body>
</html>
Working with the Web Audio API requires us to first instantiate an Audio Context. (We'll do that in the first line of our app.js file).
const ctx = new (window.AudioContext || window.webkitAudioContext)();
Then, we want to use the class syntax to create sounds that we can start and stop playing.
We'll create a class called Sound and build out its constructor. (The constructor is a function that runs when an instance gets created. It assigns various properties to the object instance.)
class Sound {
constructor() {}
}
Before we flesh this out further, it's probably worth looking at how this class is ultimately used to create an instance. (That will give context for the rest of the explanation).
So, to create an instance of a class, we invoke that class using the new keyword like so.
new Sound();
And if we pass arguments in, they'll correspond to the parameters that the constructor function receives.
So, without doing anything else, if we invoke the Sound class with the new keyword and log it to the console, we see that we get an empty Sound object.
console.log(new Sound());
Let's go back now and flesh out the constructor to give this object some properties.
As I said before, the goal is to create a sound that we can start and stop playing. We can do that by calling the createOscillator method on the Audio Context. So we'll do that here and assign it to this.osc (short for Oscillator).
class Sound {
constructor() {
this.osc = ctx.createOscillator();
}
}
If we log that to the console, our instance object is no longer empty. We now have an OscillatorNode property on our object. And this OscillatorNode is itself an object with various properties.
We use the this keyword, as in this.osc. The object instance created will be what this refers to. And we'll likely want to assign the result of invoking this Sound class to a variable.
For example, we can create const sound1 and assign that to equal the invocation of new Sound. Then, we can also create const sound2 and assign that to equal the invocation of new Sound.
const sound1 = new Sound();
const sound2 = new Sound();
And then, if we log each of these, we can see that we get two separate instances of the Sound object.
By using the this keyword when setting up our class, the object that gets returned from instantiating that class is what the this keyword refers to. And so if we assign this returned object to a variable, that variable is now what the this keyword refers to.
Going back to our Constructor, we'll also need to connect the oscillator to the Audio Context's destination. The destination represents your computer's audio output.
class Sound {
constructor() {
this.osc = ctx.createOscillator();
this.osc.connect(ctx.destination);
}
}
To make the sound audible, there are two Web Audio API methods available. Thse are, appropriately, called start and stop.
And we'll write these as methods on our Sound class.
First, we'll write the playSound method.
And that method will call this.osc.start();
class Sound {
constructor() {
this.osc = ctx.createOscillator();
this.osc.connect(ctx.destination);
}
playSound() {
this.osc.start();
}
}
NOTE: if you're trying this yourself, turn your speakers down to a reasonable volume level before you try to play the sound.
Since we've instantiated a new Sound and assigned it to a variable called sound1 , we can call the playSound method on sond1. And when we save the file, we should hear a tone, which is a sine wave.
sound1.playSound();
Now here is where Method Chaining comes into play!
Since we know that invoking new Sound returns an object, instead of writing sound1.playSound on a new line, we can chain it onto the end of the invocation of new Sound using dot notation.
const sound1 = new Sound().playSound();
If we save our file, we should hear our sound playing like before.
Let's try to create the stopSound method now. I'll use a JavaScript setTimeout to stop the oscillator from playing after 1 second, represented as 1000 milliseconds.
class Sound {
constructor() {
this.osc = ctx.createOscillator();
this.osc.connect(ctx.destination);
}
playSound() {
this.osc.start();
}
stopSound() {
setTimeout(() => {
this.osc.stop();
}, 1000);
}
}
I'm gonna go back and put playSound on its own line, like before, and then, on a new line, I'll call stopSound. So the tone should play and then stop after approximately 1 second.
const sound1 = new Sound();
sound1.playSound();
sound1.stopSound();
Let's make these three lines more concise now using method chaining.
As we did before, we can call playSound using dot notation off of newSound. And then we can go ahead and chain stopSound directly after playSound.
const sound1 = new Sound().playSound().stopSound();
If we now save the file, we can hear the sound keeps playing on and on.
Let's take a look at why it didn't work.
What we see in the console is Uncaught TypeError: Cannot read property 'stopSound' of undefined
.
Why is it saying undefined
?
If we look at the playSound method, we can see that it doesn't return anything. It does start playing the sound. However, it returns undefined.
So stopSound is trying to determine what the this keyword is referencing. All it sees is undefined.
So what we want to do to make this work is to return this from the playSound method. If we do that, stopSound will now have an object with an osc property to reference and can, therefore, go ahead and stop the sound from playing.
Conclusion
Even though we looked at method chaining here through the lens of web audio, this technique can be used all over the place. It's used extensively in the real world in various codebases and libraries.
To recap: we started by going over JavaScript's class syntax. We looked at its constructor function and the usage of the this keyword. We learned a bit about the Web Audio API and, ultimately, saw how method chaining can make our code more concise and clean.