РЕГИСТРАЦИЯ
Гость
Вход не выполнен.

Введение в JavaScript итераторы на ES6

В EcmaScript 2015 (также известном как ES6) представлена совершенно новая концепция итераторов, которая позволяет задать последовательности (ограниченные и другие) на уровне языка.
Категории раздела
0
809
Кто: modos189, Когда: , Куда: JavaScript

В EcmaScript 2015 (также известном как ES6) представлена совершенно новая концепция итераторов, которая позволяет задать последовательности (ограниченные и другие) на уровне языка.

Давайте поговорим об этом детальнее. Все мы хорошо знакомы с оператором цикла for, а многие даже знают его менее популярного брата for-in.
Последний можно использовать, чтобы помочь нам объяснить базовые принципы работы с итераторами.

for (var key in table) {
  console.log(key + ' = ' + table[key]);
}

С оператором цикла for-in есть много проблем, но самая большая, пожалуй, состоит в том, что он не дает никаких гарантий последовательности.
Попытаемся решить эту проблему с помощью итераторов.

Цикл for-of

for-of — новый синтаксис, представленный в ES6, разработанный для работы с итераторами.

for (var key of table) {
  console.log(key + ' = ' + table[key]);
}

В данном примере мы хотим увидеть такое применение итераторов, которое гарантировало бы последовательность ключей. Чтобы объект стал итерабельным, он должен применять итерабельный протокол, то есть объект (или один из объектов выше по цепочке прототипов) должен иметь свойство с ключом Symbol.iterator. Именно это использует for-of, а в данном конкретном примере это будет table [Symbol.iterator].

Symbol.iterator — другое дополнение в ES6, которое мы подробно обсудим, но как-нибудь в другой раз. Пока считайте, что Symbol.iterator — способ, чтобы задать специальные ключи, которые не будут конфликтовать с обычными ключами объектов.

Элемент объекта table [Symbol.iterator] должен быть функцией, которая соответствует протоколу итераторов, то есть она должна возвращать объект следующим образом: { next: function () {}}.

table[Symbol.iterator] = function () {
   return {
    next: function () {}
  }
}

Каждый раз когда функция next вызывается for-of, она должна возвращать объект, который выглядит так: {value: …, done: [true/false] }. Полная реализация нашего итератора выглядит следующим образом:

table[Symbol.iterator] = function () {
  var keys = Object.keys(this).sort();
  var index = 0;
  
  return {
    next: function () {
      return {
        value: keys[index], done: index++ >= keys.length
      };
    }
  }
}

Лень

Итераторы позволяют нам откладывать выполнение функции до первого вызова next. В примере выше, как только мы вызываем итератор, начинается процесс получения и сортировки ключей. Однако, что будет, если next не будет вызвана? Просто потратим время зря. Поэтому давайте оптимизируем код:

table[Symbol.iterator] = function () {
  var _this = this;
  var keys = null;
  var index = 0;
  
  return {
    next: function () {
      if (keys === null) {
        keys = Object.keys(_this).sort();
      }
      
      return {
        value: keys[index], done: index++ >= keys.length
      };
    }
  }
}

Разница между for-of и for-in

Важно понимать разницу между for-of и for-in. Приведем простой, но показательный пример, почему это так.

var list = [3, 5, 7];
list.foo = 'bar';
 
for (var key in list) {
  console.log(key); // 0, 1, 2, foo
}
 
for (var value of list) {
  console.log(value); // 3, 5, 7
}

Как видите, for-of выдает только значения, которые есть в массиве, и опускает все остальные свойства. Это происходит в следствии того, что итератор массива возвращает только ожидаемые параметры.

Встроенные итераторы

String, Array, TypedArray, Map, Set содержат встроенные итераторы, потому что объекты их прототипов содержат метод Symbol.iterator.

var string = "hello";
 
for (var chr of string) {
  console.log(chr); // h, e, l, l, o
}

Конструкт Spread

Оператор Spread так же может принимать итераторы. Это можно использовать примерно так:

var hello = 'world';
var [first, second, ...rest] = [...hello];
console.log(first, second, rest); // w o ["r","l","d"]

Бесконечный итератор

Применить бесконечный итератор так же легко, как никогда не возвращать done: true. Конечно, необходимо убедиться, что нигде не образовался бесконечный цикл.

var ids = {
  *[Symbol.iterator]: function () {
    var index = 0;
    
    return {
      next: function () {
        return { value: 'id-' + index++, done: false };
      }
    };
  }
};
 
var counter = 0;
 
for (var value of ids) {
  console.log(value);
  
  if (counter++ > 1000) { // let's make sure we get out!
    break;
  }
}

Генераторы

Если вы еще не знакомы с генераторами в [url=https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*]ES6[/url], то самое время почитать документацию. В двух словах, генераторы — это функции, выполнение которых можно приостановить, а затем возобновить. В последнее время генераторы — самая обсуждаемая функциональность в ES6. Контекст сохраняется между перезапусками. Генераторы содержат оба протокола — Iterable и Iterator. Давайте рассмотрим простой пример:

function* list(value) {
  for (var item of value) {
    yield item;
  }
}
 
for (var value of list([1, 2, 3])) {
  console.log(value);
}
 
var iterator = list([1, 2, 3]);
 
console.log(typeof iterator.next); // function
console.log(typeof iterator[Symbol.iterator]); // function
 
console.log(iterator.next().value); // 1
 
for (var value of iterator) {
  console.log(value); // 2, 3
}

Принимая во внимание код выше, мы можем переписать код итератора, который сортирует ключи, с использованием генераторов:

table[Symbol.iterator] = function* () {
  var keys = Object.keys(this).sort();
  
  for (var item of keys) {
    yield item;
  }
}

Итог

Итераторы открывают целое новое измерение в работе с циклами, генераторами и наборами переменных. Их можно передавать, задавать как класс описывает список параметров, создавать “ленивые” или бесконечные списки и т.д. и т.п.

ES6 сегодня

Как можно ли воспользоваться возможностями ES6 уже сегодня? Использование транспайлеров стало нормой в последние несколько лет. Ни простые разработчики, ни крупные компании не стесняются их применять. Babel — транспайлер из ES6 в ES5, который поддерживает все нововведения ES6.

Если используете Browserify, то добавить Babel вы сможете всего за пару минут. Конечно же, есть поддержка практически для любого билда с Node.js. Например: Gulp, Grunt и многие другие.

Как насчет браузеров?

Большинство браузеров постепенно добавляют новые функции, но полной поддержки пока нет ни у кого. Что, теперь ждать? Возможно. Имеет смысл начать использовать функции языка, которые станут универсальными через год-два. Это позволит вам комфортно перейти на новый этап. Однако, если вам нужен 100% контроль над исходным кодом, то пока лучше подождать и использовать ES5.

Перевод подготовили: greebn9k(Сергей Грибняк), silmarilion(Андрей Хахарев)