Итераторы в Javascript

В ES6 появились итераторы, которые позволяют извлекать данные в необходимом нам порядке. В связи с этим стала доступной возможность задать нужную нам логику перебора конкретного итерируемого объекта.

Таким объектом может быть не только массив, но и большинство сущностей которые можно перебрать, например, списки NodeList в DOM и строки.

Синтаксис:

У итерируемого объекта есть специальный метод, который возвращает объект. Для доступа к этому методу используется специальный символ Symbol.iterator:

let table = {};

table[Symbol.iterator] = function () {
 return {
   next: function () {
    return{
      done: true, //или false
      value: 1,// Может быть любой значение
    }
  }
 }
}

Как мы видим, у итератора есть метод next(), возвращающий объект, у которого есть два свойства done и value.

Свойство done указывает на наличие значения в последующей итерации, а value содержит следующий элемент последовательности.

Наверное вы подумали о том, что это запись выглядит достаточно странно и почему бы вместо [Symbol.iterator] не использовать просто метод .iterator().

Дело в том, что символ итератора позволяет сделать этот метод уникальным и не доступным для обычного (привычного нам) вызова. Тем самым, если в вашем коде уже есть метод .iterator(), то он не будет конфликтовать и итератором объекта, который заложен в спецификации JavaScript.

Как работает итератор?

Создадим массив с именем arr и объявим новый итератор c именем it:

let arr = [1,2,3];//массив
let it = arr[Symbol.iterator]();// объявление итератора

console.log(it.next()); // { value: 1, done: false }
console.log(it.next()); // { value: 2, done: false }
console.log(it.next()); // { value: 3, done: false }
console.log(it.next()); // { value: undefined, done: true }

В данном примере видно, что итератор it не сообщает о завершении своей работы, когда дошел до значения 3 ,т.к done равен false, для этого нам нужно еще раз вызвать метод next() что бы получить значение false. Фактически нам нужно выйти за пределы массива.

Значение false в последнем вызове метода next() дает программе явно понять, что перебор объекта закончен и нужно выходить из цикла.

Для того, что бы понять происходящее предлагаю рассмотреть пример использования итератора в привычном нам цикле for

for(var v, res; (res = it.next()) && !res.done;){
  v = res.value;
  console.log(v);//1, 2, 3
}

Перед каждой итерацией вызывается метод next() и происходит сравнение с переменной res.done и если она имеет значение true, то выражение получает false и цикл завершается.

В этом примере наглядно показано, что в случае с итераторами, лучше не возвращать в предполагаемом нами последнем значении done: true, т.к. если итератор вернет { done: true, value: 3 }, то итератор не выведет значение 3 и именно на нем завершит цикл. Именно по этому нужно программе «сказать», что нужно сделать последнюю финальную итерацию и уже потом выходить из цикла.

Новый цикл for…of

Что бы облегчить работу и итераторами в ES6 появился новый цикл for..of, который как раз таки работает с итерируемыми объектами и при каждом проходе вызывает метод next() и если в последней итерации получает done: true, то выходит из цикла.

Данный цикл не будет работать с обычными объектами, и если вам надо перебрать свойства объекта, то на это есть for…in.

Пользовательские итераторы

Помимо стандартных итераторов, которые заложены в новой спецификации JavaScript, можно создавать собственные итераторы, например:

const tasks = {
   home: [
       "Забрать Чампа", 
       "Зайти в прачечную", 
       "Исповедаться в грехах"
        ],
   work: [
       "Сделать отчет", 
       "Не проспать", 
       "Сходить в столовую"
       ],
   study: [
       "Сдать алгебру", 
       "Сходить на химию"
   ],
   [Symbol.iterator](){

        const tasks = Object.values(this);

        let i = 0;
        let taskIndex = 0;

        return{
           next(){

              if(i === tasks[taskIndex].length){
                 i = 0;
                 taskIndex++;
              }

              if(taskIndex === tasks.length){
                 return{
                done: true
             }
          }

          return{
             value: tasks[taskIndex][i++],
             done: false
          }                 
       }
     }
   }
}  

for(let task of tasks){
 console.log(task);
}

Этот пример наглядно показывает, как с помощью итератора можно перебрать объект, значениями которого являются массивы. Тем самым из обычного объекта можно сделать итерируемый, который будет работать в цикле for…of и перебираться по нужной нам логике.