Since the introduction of modern JavaScript with ES5 and later ES6, common language patterns and structures were introduced that lifted the limited expressive power of the original JavaScript language to the level of more robust programming languages.
In the first part of this blog post, I touched on Map
and Set
, in this second part, I will take a look at what is possible in modern JavaScript when it comes to iterating over collections.
Array Loops
A very powerful and more readable loop structure was introduced, the for..of
loop:
let values = [ 1, 2, 3, 4, 5]
for (const value of values) {
console.log(value) // 1 2 3 4 5
}
One of the best parts, is that it works naturally with code that uses async/await
syntax:
for (const value of values) {
await saveToDatastore(value)
}
Which makes for very concise and readable code.
Iterables
Now, with arrays we already have a datatype that allows a collection of data to loop over, but a lot of times, data is of unknown size, even indefinite.
This is where the Iterable
protocol comes in very handy. An Iterable
can be any object that implements the @@iterator
method (accessible via [Symbol.iterator]
property).
Calling that method should return an Iterator
, which is simply an object that has one method: next()
. Each call to next()
will return the next value, plus a boolean indicating if this was the last value.
String
, Array
and Set
are all examples of an Iterable
, but imagine a class that will list the values at the even indexes of an array:
class SimpleIterator {
constructor(data) {
this.data = data;
}
[Symbol.iterator]() {
let index = 0;
return {
next: () => {
if (index < this.data.length) {
const value = this.data[index]
index += 2
return {value: value, done: false}
} else {
return {done: true}
}
}
}
}
}
const simple = new SimpleIterator([ 1, 2, 3, 4, 5 ]);
for (const val of simple) {
console.log(val); // '1' '3' '5'
}
Await Loops
We can go one step further, by introducing an AsyncIterator
, which is basically an iterator that returns a Promise that resolves with {value, done}
, just like its synchronous counterpart.
Imagine we have a datastore that allows us to query blog posts and returns us a cursor (or stream of blog posts) of unknown length.
If that datastore implements an AsyncIterator
, we can combine our loops and async/await to truly create an asynchronous loop:
const cursor = blogposts.query({ tag: 'engineering'})
for await (const post of cursor) {
console.log(post.title)
}
Destructuring and Spread
When working with arrays of data, modern JavaScript gives you two very useful language constructs that allow you to write more concise code: Destructuring
and Spread Syntax
.
Assigning values to multiple variables can quickly lead to many lines of code, even though the assignment consists of one logical block.
let values = [ 1, 2, 3, 4 ]
let a = values[0]
let b = values[1]
let c = values[2]
let d = values[3]
Wouldn't it be nice if this could be done in one go? With Array Destructuring
, you can!
let a, b
[ a, b ] = [ 1, 2 ]
Extra values are ignored, missing values are undefined:
let values = [ 1, 2, 3 ]
let a, b, c, d
[ a, b ] = values
console.log(a) // 1
console.log(b) // 2
[ a, b, c, d ] = values
console.log(c) // 3
console.log(d) // undefined
Array Destructuring
is especially useful if you want to swap values without the need for an intermediate variable:
let a = 1
let b = 2
[ a, b ] = [ b, a ]
console.log(a) // 2
console.log(b) // 1
But what if the length of your data collection is unknown and you still want to use all values?
The Spread syntax
is a powerful way to use arrays of values and assign them to variables:
let values = [ 1, 1, 2, 3, 5, 8, 13 ]
let first, second, rest
[ first, second, ...rest ] = values
console.log(second) // 1
console.log(first) // 1
console.log(rest) // [ 2, 3, 5, 8, 13 ]
This can be especially useful if you want to pass an array of values as separate parameters to a function call:
let values = [ 1, 2 ]
function add(first, second) {
return first + second
}
console.log(add(...values)) // 3
It also works with any Iterable, so for example with Set
:
let values = new Set()
values.add(1)
values.add(2)
console.log(add(values)) // 3
It's important to not confuse Spread
with the Rest Parameter
syntax for functions, which is kind of the opposite of the spread, but is still handy to give you an array of input values of variable length:
function remainder(first, second, ...params) {
return params
}
console.log(remainder(1, 2, 3, 4, 5)) // [ 3, 4, 5 ]
Conclusion
Since the introduction and wide-spread adoption of async/await and iterables, there is nothing stopping you from using these powerful language features, especially when used in NodeJS applications.
As always, we hope you liked this article and if you have anything to add, maybe you are suited for a Developer position in Notificare. We are currently looking for a Core API Developer, check out the job description. If modern Javascript is your thing, don't hesitate to apply!