JavaScript Iterables

The protocol of iterations enables JavaScript objects to define and customize their iteration behavior like what values are looped over in for...of.

Arrays are considered iterable. But, there are also other iterables (for instance, strings).

Describing Symbol.iterator

Let’s try to create an iterable.

For instance, there is an object, which is not an array but can be suitable for for...of.

Let’s see an example of a range object, representing an interval of numbers:

let range = {
  from: 0,
  to: 10
}; 
// We want the for..of to work:
// for(let num of range) ... num=0,1,2,3,4,5,6,7,8,9,10

For transforming the range into iterable, it is necessary to add a method to the object called Symbol.iterator.

These are the steps:

  1. Once for..of begins, the method is called once. It should return an iterator.
  2. Then, for..of can operate with the returned object.
  3. Once, for..of expects the next value, next() is called on that object.
  4. Its result should include the {done: Boolean, value: any} form.

The full performance for the range with remarks will look as follows:

let range = {
  from: 0,
  to: 5
};
// 1. called to for..of initially calls this
range[Symbol.iterator] = function () { 
  //returns an iterator object:
  // 2.for..of works only with this iterator, querying it for next values
  return {
    currentValue: this.from,
    lastValue: this.to, 
    // 3. next() is called at each iteration with a for..of loop
    next() {
      // 4. it should return the value as an object {done:.., value :...}
      if (this.currentValue <= this.lastValue) {
        return {
          done: false,
          value: this.currentValue++
        };
      } else {
        return {
          done: true
        };
      }
    }
  };
};
for (let num of range) {
  console.log(num); // 0, then 1, 2, 3, 4, 5
}

The most essential feature of iterables is the segregation of concerns. It considers that the iterator object is distinct from the ones that it iterates over. It is possible to merge them using the range like an iterator for making the code easier, as shown below:

let range = {
  from: 0,
  to: 5,
  [Symbol.iterator]() {
    this.currentValue = this.from;
    return this;
  }, 
  next() {
    if (this.currentValue <= this.to) {
      return {
        done: false,
        value: this.currentValue++
      };
    } else {
      return {
        done: true
      };
    }
  }
};
for (let num of range) {
  console.log(num); // 0, then 1, 2, 3, 4, 5
}

The main disadvantage of this case is that it is not possible to have two for..of that run over the object at the same time.

There can be infinite iterators. For example, you can transform range into infinite with range.to = Infinity. There is an alternative option, too: you can create an iterable object, which generates an infinite sequence of pseudorandom numbers. So, no limitations are applied on next, and it may return more values. Consequently, the for..of loop will also become endless over an iterable like this. But it can be stopped with break.

Strings as Iterables

The most widely used iterables are strings and arrays.

The for..of may loop over its characters, in case of a string:

for (let char of "W3Docs") {
  // triggers 6 times: once for each character
  console.log(char); // W, then 3, then D, then o, then c, then s
}

It will work properly, like this:

let str = 'ȬẂ';
for (let char of str) {
  console.log(char); // Ȭ, and then Ẃ
}

Array-likes and Iterables

In this paragraph, we will look through the terms iterables and array-likes. They may seem similar but are very different.

Array-likes are considered objects that contain indexes and length. Iterables are objects that are capable of implementing the Symbol.iterator method. This method was described above.

While using JavaScript, you can meet a lot of array-lie and iterable objects. For instance, strings can both be iterables and array-likes. On the contrary, an iterable can never become an array-like, and vice versa.

Let’s check out an example of an object that is an array-like but not an iterable:

let arrLike = { // has indexes and length => array-like

  0: "Welcome",
  1: "to",
  2: "W3Docs",
  length: 3
};
// Error (no Symbol.iterator)
for (let item of arrLike) {
  console.log(item);
}

As a rule, both iterables and array-likes are not arrays, as they don’t contain pop, push, and so on.

Describing Array-form

Array-form is a generic method used for taking an iterable or array-like value and making a real array from it. Afterward, array methods can be called on it. Here is an example of using array-form:

let arrLike = {
  0: "Welcome",
  1: "to",
  2: "W3Docs",
  length: 3
};
let arr = Array.from(arrLike); // (*)
console.log(arr.pop()); // W3Docs (method operates)

At the (*) line, Array.from accepts the object, examining whether it’s an array-like or an iterable, then creates a new array, copying all the items into it. Let’s see how the same takes place with an iterable:

let range = {
  from: 0,
  to: 5,
  [Symbol.iterator]() {
    this.currentValue = this.from;
    return this;
  }, 
  next() {
    if (this.currentValue <= this.to) {
      return {
        done: false,
        value: this.currentValue++
      };
    } else {
      return {
        done: true
      };
    }
  }
};
//confirming that the range is obtained from the above example
let arr = Array.from(range);
console.log(arr); // 0,1,2,3,4,5 ;array toString conversion works

The full syntax of Array.from looks like this:

Array.from(obj[, mapFunc, thisArg])

The second argument mapFn is a function, which may be attached to every element before adding it to the array. the third argument thisArg allows setting this for that. Here is an example:

let range = {
  from: 0,
  to: 5,
  [Symbol.iterator]() {
    this.currentValue = this.from;
    return this;
  },
  next() {
    if (this.currentValue <= this.to) {
      return {
        done: false,
        value: this.currentValue++
      };
    } else {
      return {
        done: true
      };
    }
  }
};
// provided that the range is taken from the above example
// square of each number
let arr = Array.from(range, num => num * num);
console.log(arr); // 0,1,4,9,16,25

Now, let’s turn a string into an array of characters with the help of Array.from:

let str = 'ȬẂ';
// splits the str into an array of characters
let chars = Array.from(str);
console.log(chars[0]); // Ȭ
console.log(chars[1]); // Ẃ
console.log(chars.length); // 2

On the contrary to the str.split method, it depends on the iterable nature of the string.

It acts the same as:

let str = 'ȬẂ';
let charsArr = []; // Array.from internally does the same loop
for (let char of str) {
  charsArr.push(char);
}
console.log(charsArr);

Summary

Iterables are the objects that may be used in the for..of.

As a rule, they should perform the Symbol.iterator method. This method is, generally, called automatically by for..of. But, it can also be done directly. Objects, having indexed properties, and length are considered array-like. They can also include other properties and methods, but they are not real arrays. For transforming them into arrays, you can use the Array.from(obj[, mapFn, thisArg]) method.

The mapFn and thisArg arguments allow applying a function to each of them.




Do you find this helpful?

Related articles