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

Прежде чем рассмотреть проблему и понять, как ее решить, давайте начнем с теории…

Итераторы

Итерируемые объекты — это объекты, которые можно повторять с помощью цикла for...of.

Все, что является итерируемым, является объектом, в котором будет метод Symbol.iterator, и когда мы пытаемся повторить его или хотим, чтобы он вел себя как итерируемый (используя цикл for...of), будет вызван метод Symbol.iterator, который вернет объект (iterator ). Возвращенный объект будет иметь метод next, который создаст объект со значением и статусом итерации (используя boolean) — независимо от того, присутствует ли какое-либо другое значение или нет для итерации.

Метод (Symbol.iterator), возвращающий объект (итератор), в котором есть другой метод (next) 🤔 — сложный, верно?

Давайте лучше поймем приведенные выше утверждения, решив проблему с использованием итераторов.

Проблема:

Q. Can you create a range(from, to) that makes the following work?
for (let num of range(1, 4)) {
  console.log(num)  
}
// 1
// 2
// 3
// 4
This is a simple one, could you think of more fancy approaches other than for-loop?

Простейшим решением вышеуказанной проблемы будет следующее:

function range(from, to){
  const arr = [];
  for(let i = from; i <= to; i++){
    arr.push(i);
  }
  return arr;
}

Посмотрите на условие задачи еще раз, мы не обязаны возвращать массив, но что-то итерируемое было бы хорошо, так как мы будем использовать цикл for...of.

Теперь мы напишем функцию range, которая будет принимать 2 числа в качестве аргументов и возвращать итерируемый объект.

// to make range() iterable
function range(from, to) {
  return {    
    [Symbol.iterator]: function(){
      /* This returned object is known as the "iterator object" in the case of iterators and the "generator object" in the case of generators. */
      return {
        from,
        to,
        next: function() {
          if(this.from <= this.to){
              return {value: this.from++, done: false}
          }else{
              return {done: true}
          }
        }
      }      
    }
  };
}
for (const item of range(1, 4)) {
  console.log(item);  
}

В приведенном выше коде всякий раз, когда мы пытаемся выполнить итерацию с использованием цикла for...of,

  • он вызывает метод Symbol.iterator один раз в первый раз и получает объект iterator.
  • В следующий раз for...of использует метод next для получения следующего значения во время итерации.

Не путайте массивы с итерируемыми объектами. Iterables — это объекты, которые реализуют в нем метод Symbol.iterator. Массивы и строки имеют встроенную реализацию Symbol.iterator вместе со многими другими методами и свойствами, не обязательно, что другие итерации будут иметь то же самое.

Генераторы

Функция-генератор возвращает объект, известный как «объект-генератор», и содержит в себе метод next.

Пример:

function *generatorExample(){
  yield 1;
  yield 2;
}
const generatorObject = generatorExample();
console.log(generatorObject.next()); // {value: 1, done: false};
console.log(generatorObject.next()); // {value: 2, done: false};
console.log(generatorObject.next()); // {value: undefined, done: true};

Когда вызывается метод next, он выполняет функцию до первого выхода, затем приостанавливает выполнение и возвращает объект с двумя свойствами:

  • значение: полученное значение.
  • done: логическое значение, указывающее, завершено ли выполнение функции или нет.

Функции генератора можно идентифицировать по * в объявлении функции сразу после ключевого слова function (функция*) или перед именем функции (функция *имя) без пробела.

Та же самая проблема, упомянутая выше, может быть легко решена с помощью функций генератора следующим образом:

function range(from, to){
  return {
    [Symbol.iterator]: function* (){
      for(let i = from; i <= to; i++){
        yield i;
      }
    }
  }  
}
for (const item of range(1, 4)) {
  console.log(item);  
}
// 1
// 2
// 3
// 4

Функция range по-прежнему возвращает объект, в котором есть метод Symbol.iterator. Но разница в том, что Symbol.iterator теперь выполняет функцию генератора. А как мы знаем, функция-генератор возвращает объект-генератор с методом next в себе — который нам пришлось реализовать явно для итераторов.

Генераторы также являются итерируемыми.

Тогда мы можем использовать цикл for...of с генераторами.

function *generatorExample(){
  yield 1;
  yield 2;
}
const generatorObject = generatorExample();
for(const item of generatorObject){
  console.log(item);
}
// 1
// 2

Поскольку генератор также является итерируемым, решение можно еще больше упростить, поскольку нам не нужно явно реализовывать метод Symbol.iterator.

function *range(from, to){
  for(let i = from; i <= to; i++){
    yield i;
  }
}
for (const item of range(1, 4)) {
  console.log(item);  
}
// 1
// 2
// 3
// 4

На этом пока все 😀. Спасибо, что дочитали до сих пор🙏.

Если вы хотите узнать о них больше, обратитесь к разделу Итераторы и генераторы — MDN.

Поделитесь этим блогом со своей сетью, если вы нашли его полезным, и не стесняйтесь комментировать, если у вас есть какие-либо сомнения по теме.

Вы можете связаться 👋 со мной на GitHub, Twitter, Linkedin