Коллекции и итераторы

Итераторы

Последнее обновление: 20.11.2023

Итераторы представляют абстракцию для перебора наборов данных и применяются для организации последовательного доступа к элементам наборов данных - массивам, объектам Set, Map, строкам и т.д..Так, благодаря итераторам мы можем перебрать набор данных (например, массив) с помощью цикла for-of:

const people = ["Tom", "Bob", "Sam"];
for(const person of people){
	console.log(person);
}

В цикле for-of справа от оператора of указывается набор данных или перебираемый объект (то, что назвается Iterable), из которого в цикле мы можем получить отдельные элементы. Но эта возможность перебора некоторого объекта, как, например, массива в примере выше, реализуются благодаря тому, что эти объекты применяют итераторы. Рассмотрим подробнее, что представляют итераторы и как можно создать свой итератор.

Получение итератора

Любой итерируемый объект (например, массив, Map, Set и т.д.) хранит в свойстве Symbol.iterator функцию, которая возвращает связанный с объектом итератор:

const people = ["Tom", "Bob", "Sam"];
// получаем итератор массива
const iterator = people[Symbol.iterator]();
console.log(iterator);  // Array Iterator {}

Здесь получаем итератор массива, поэтому на консоль будет выведено что-то наподобие Array Iterator {}

Другой пример - строка тоже представляет перебираемый объект, которую можно перебрать посимвольно:

const username = "Tom";
for(char of username){
    console.log(char);
}

Соответственно для строки мы тоже можем получить итератор:

const username = "Tom";
// получаем итератор строки
const iterator = username[Symbol.iterator]();
console.log(iterator);  // StringIterator {}

Итератор строки представляет тип StringIterator. Аналогичным образом можно получать итераторы и для других типов перебираемых объектов.

Стоит отметить, что у различных типов могут быть различные дополнительные методы для получения итератора. Например, у массивов есть метод entries(), который также возвращает итератор массива:

const people = ["Tom", "Bob", "Sam"];
console.log(people.entries()); // Array Iterator {}

Метод next итераторов

Итераторы предоставляют метод next(), который возвращает объект с двумя свойствами: value и done

{value, done}

Свойство value хранит собственно значение текущего перебираемого элемента. А свойство done указывает, есть ли еще в коллекции объекты, доступные для перебора. Если в наборе еще есть элементы, то свойство done равно false Если же доступных элементов для перебора больше нет, то это свойство равно true, а метод next() возвращает объект

{done: true}

Например:

const people = ["Tom", "Bob", "Sam"];
const iter = people[Symbol.iterator]();
const result = iter.next();
console.log(result);    // {value: "Tom", done: false}

В данном случае вызываем метод next() и получаем из итератора первыый результат:

{value: "Tom", done: false}

Здесь мы видим, что текущий объект представляет строку "Tom", а значение done: false указывает, что в массиве еще есть элементы для перебора.

Мы можем последовательно несколько раз вызвать метод next() для получения других элементов массива:

const people = ["Tom", "Bob", "Sam"];
const iter = people[Symbol.iterator]();
console.log(iter.next());    // {value: "Tom", done: false}
console.log(iter.next());    // {value: "Bob", done: false}
console.log(iter.next());    // {value: "Sam", done: false}
console.log(iter.next());    // {value: undefined, done: true}

Консольный вывод программы:

{value: "Tom", done: false}
{value: "Bob", done: false}
{value: "Sam", done: false}
{value: undefined, done: true}

Здесь мы видим, что при каждом новом вызове метода next() мы получаем из массива следующий объект. А когда объектов для перебора больше не останется, то свойство done будет равно true.

Используя метод next(), мы сами можем перебрать все объекты массива:

const people = ["Tom", "Bob", "Sam"];
const iter = people[Symbol.iterator]();
while(!(item = iter.next()).done){
    console.log(item.value);
}

Здесь в цикле while из метода next() итератора получаем текущий объект в переменную item: item = items.next()

И смотрим на ее свойство done - если оно равно false (то есть в наборе еще есть элементы), то продолжаем цикл

while(!(item = iter.next()).done){

В цикле обращаемся к свойству value полученного объекта

console.log(item.value);

Консольный вывод:

Tom
Bob
Sam

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

Создание своего итератора

Для примера реализуем итератор, который перебирает массив с конца:

const people = ["Tom", "Bob", "Sam"];

function reverseArrayIterator(array) {  
    let count = array.length; 
    return {    
        next: function(){      
            if (count > 0) {   
                return {          
                    value: array[--count],          
                    done: false        
                };     
            } 
            else {       
                return {          
                    value: undefined,           
                    done: true        
                };           
            }    
        }  
    }
};
const iter = reverseArrayIterator(people);
while(!(item = iter.next()).done){
    console.log(item.value);
}

Здесь сначала инициализируется переменная count, которая количество перебранных элементов массива. Первоначально переменная имеет значение, равное длине массива.

Далее функция возвращает объект итератора. Его метод next() реализует поведение итерации: если счетчик count больше 0 (то есть имеются еще элементы для перебора), то next() возвращает объект, свойство done которого имеет значение false (поскольку итератор еще не достиг конца или точнее начала массива), а свойство value содержит соответствующий элемент из массива, на который указывает переменная count после декремента.

Когда переменная count станет равна 0 (т. е. итератор достиг конца), next() возвращает объект, у которого свойство done имеет значение true, а свойство value имеет значение undefined.

Таким образом, мы получим итератор, который перебирает объекты массива с конца. Консольный вывод:

Sam
Bob
Tom

Однако при выполнении цикла for..of элементы массива по прежнему перебираются с начала. Применим наш итератор глобально, чтобы он также использовался в цикле for..of:

const people = ["Tom", "Bob", "Sam"];

function reverseArrayIterator() { 
    const array = this;
    let count = array.length; 
    return {    
        next: function(){      
            if (count > 0) {   
                return {          
                    value: array[--count],          
                    done: false        
                };     
            } 
            else {       
                return {          
                    value: undefined,           
                    done: true        
                };           
            }    
        }  
    }
};
// меняем итератор для массива people
people[Symbol.iterator]=reverseArrayIterator;
for(person of people){
    console.log(person);
}

Здесь сделано два ключевых изменения. Во-первых, нам надо внутри итератора получить текущий объект через this:

const array = this;

Созданную функцию итератора надо присвоить свойству Symbol.iterator:

people[Symbol.iterator]=reverseArrayIterator;

Создание итерируемых объектов

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

Для создания перебираемого объекта нам надо определить в объекта метод [Symbol.iterator](). Этот метод собственно и будет представлять итератор:

const iterable = {
  [Symbol.iterator]() {
    return {
      next() {
			// если еще есть элементы
			return { value: ..., done: false };
			// если больше нет элементов
			return { value: undefined, done: true };
      }
    };
  }
};

Метод [Symbol.iterator]() возвращает объект, который имеет метод next(). Этот метод возвращает объект с двумя свойствами value и done.

Если в нашем объекте есть элементы, то свойство value содержит собственно значение элемента, а свойство done равно false.

Если доступных элементов больше нет, то свойство done равно true.

Например, реализуем простейший объект с итератором, который возвращает некоторый набор чисел:

const iterable = {
  [Symbol.iterator]() {
    return {
      current: 1,
	  end: 3,
      next() {
        if (this.current <= this.end) {
          return { value: this.current++, done: false };
        }
        return { done: true };
      }
    };
  }
};

Здесь итератор фактически возвращает числе от 1 до 3. Для отслеживания текущего элемента в объекте, который возвращается методом , определены два свойства:

current: 1,
end: 3,

Свойство current собственно хранит значение текущего элемента. А свойство end задает предел. То есть в данном случае итератор возвращает числа от 1 до 3.

В методе next(), если текущее значение меньше или равно редельному значению, возвращаем объект

return { value: this.current++, done: false };

Инкремент this.current++ приведет к тому, что при следующем вызове метода next значение current будет на единицу больше.

Если достигнут предел, то возвращаем объект

return { done: true };

Это будет указывать, что объектов больше нет.

Получим из итератора возвращаемые им элементы:

const myIterator = iterable[Symbol.iterator]();	// получаем итератор
console.log(myIterator.next());	// {value: 1, done: false}
console.log(myIterator.next());	// {value: 2, done: false}
console.log(myIterator.next());	// {value: 3, done: false}
console.log(myIterator.next());	// {done: true}

Здесь сначала получаем итератор в константу myIterator. Затем при обращении к ее методу next() последовательно получаем все элементы. При четвертом вызове метода next условный перебор элементов в итераторе закончен, и метод возвращает объект {done: true}.

Однако если мы хотим перебрать наш объект и получить из него его элементы, то нам не надо обращаться к методу next(). Поскольку объект iterable реализует итератор, то его можно перебрать с помощью цикла for-of:

const iterable = {
  [Symbol.iterator]() {
    return {
      current: 1,
	  end: 3,
      next() {
        if (this.current <= this.end) {
          return { value: this.current++, done: false };
        }
        return { done: true };
      }
    };
  }
};
for (const value of iterable) {
  console.log(value);
}

Консольный вывод:

1
2
3

Цикл for-of автоматически обращается к методу next() и извлекает значение.

Рассмотрим еще один пример:

// объект-компания
const company = {
    // массив работников
    employees: [
        {name: "Tom", age: 39, position: "Senior Developer"},
        {name: "Bob", age: 43, position: "Middle Developer"},
        {name: "Sam", age: 28, position: "Junior Developer"},
    ]
};
// устанавливаем итератор
company[Symbol.iterator] = function() {
    const array = this.employees; // получаем массив работников
    let current = 0;
    return {
      next() {
        if (current < array.length) {
          return { value: array[current++].name, done: false };
        }
        return { value:undefined, done: true };
      }
    };
  };
for (const employee of company) {
  console.log(employee);
}

Здесь объект company представляет условную компанию, в которой есть массив работников - массив employee. Допустим, с помощью итератора мы хотим получать имя каждого работника. Для этого для объекта company устанавливаем функцию итератора, которая перебирает все элементы из массива employees. Консольный вывод программы:

Tom
Bob
Sam
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850