ES6 в деталях: итераторы и циклы for-of

ES6 в деталях — это цикл статей о новых возможностях языка программирования JavaScript, появившихся в 6 редакции стандарта ECMAScript, кратко — ES6.

Как перебрать все элементы массива? Двадцать лет назад, когда JavaScript только появился, мы бы сделали так:

for (var index = 0; index < myArray.length; index++) {
    console.log(myArray[index]);
}

Начиная с ES5, можно воспользоваться встроенным методом forEach:

myArray.forEach(function (value) {
    console.log(value);
});

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

Разумеется, было бы хорошо, если бы синтаксис цикла for просто позволял перебрать все элементы массива.

Как насчёт цикла for-in?

for (var index in myArray) {    // вообще-то, не стоит так делать
    console.log(myArray[index]);
}

Это плохое решение, по нескольким причинам:

Если кратко, for-in рассчитан на работу с обычными объектами Object с именами свойств в виде строк. Для массивов он подходит не так хорошо.

Могущественный цикл for-of

Помните, на прошлой неделе я обещал, что ES6 не сломает тот код на JS, что вы уже написали? Вот, миллионы сайтов зависят от поведения for-in, да, даже от того, как он работает с массивами. Так что о том, чтобы «поправить» for-in и сделать его более полезным для массивов, не было и речи. Единственный способ, которым ES6 может улучшить ситуацию — добавить какой-нибудь новый синтаксис.

И вот так он выглядит:

for (var value of myArray) {
    console.log(value);
}

Хмм… После моего интригующего описания вы, наверное, ожидали чего-то более впечатляющего? Что ж, давайте взглянем, есть ли у for-of козырь в рукаве. Для начала отметим, что:

Циклы for-in нужны для перебора свойств объекта.

Циклы for-of нужны для перебора данных, например, значений массива.

Но это ещё не всё.

Использование for-of с другими коллекциями

for-of не только для массивов. Он также работает с большинством массивоподобных объектов, вроде списковNodeList в DOM.

Ещё он работает со строками, рассматривая строку как набор символов Unicode:

for (var chr of "😺😲") {
    alert(chr);
}

Он также работает с объектами Map и Set.

Ой, простите. Вы никогда не слышали про объекты Map и Set? Что ж, они появились в ES6. Когда-нибудь мы посвятим им отдельную статью. Если вы уже работали со словарями или списками в других языках, то не ожидайте ничего особо нового.

К примеру, объект Set хорош для устранения повторяющихся значений:

// создаём список из массива слов
var uniqueWords = new Set(words);

Теперь, когда у вас есть список, возможно, вы захотите перебрать всё его содержимое. Легко:

for (var word of uniqueWords) {
    console.log(word);
}

С Map немного иначе: данные внутри — это пары ключ-значение, так что вам пригодится деструктурирование для распаковки ключа и значения в две отдельные переменные:

for (var [key, value] of phoneBookMap) {
    console.log("У " + key + " номер телефона: " + value);
}

Деструктурирование — это ещё одна возможность, введённая в ES6 и отличная тема для будущей статьи. Надо бы записывать, а то забуду.

Уже сейчас вы можете сложить представление: в JS уже есть немало различных классов-коллекций, а скоро появится ещё больше. Циклы for-of разработаны как рабочая лошадка для работы со всеми ними.

for-of не будет работать с обычными объектами, но если вам нужно перебрать все свойства объекта, вы можете использовать или for-in (для чего он и предназначен), или встроенную функцию Object.keys():

// сбрасываем все перечислимые свойства объекта в консоль
for (var key of Object.keys(someObject)) {
    console.log(key + ": " + someObject[key]);
}

Под капотом

«Хорошие художники копируют, великие художники воруют» — Пабло Пикассо

Главная особенность ES6 в том, что функциональные возможности, добавляемые в язык, не взялись с потолка. Большинство из них уже прошли испытания и успешно зарекомендовали себя в других языках.

К примеру, цикл for-of напоминает похожие конструкции из C++, Java, C# и Python. Подобно им, он работает с несколькими различными структурами, предоставленными самим языком и его стандартной библиотекой. Но это также и точка для расширения в языке.

Так же, как и выражения for/foreach в этих языках, работа for-of полностью основана на вызовах методов. Общее в объектах Array, Map, Set и других, о которых мы говорили, в том, что у них всех есть метод-итератор.

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

Подобно тому, как если добавить метод myObject.toString() в любой объект, и JS внезапно узнает как приводить этот объект к строке, можно добавить метод myObject[Symbol.iterator]() в любой объект, и JS внезапно узнает, как этот объект использовать для цикла.

К примеру предположим, что вы пользуетесь jQuery, и хотя вам очень нравится .each(), вам хотелось бы, чтобы for-of работал и на объектах jQuery. Вот, что нужно сделать:

// Раз уж объекты jQuery похожи на массивы,
// назначим им тот же итератор, что и у массивов
jQuery.prototype[Symbol.iterator] =
  Array.prototype[Symbol.iterator];

Хорошо, я знаю, о чём вы думаете. О том, что синтаксис с [Symbol.iterator] выглядит странно. Почему именно так? Тут дело в имени метода. Комитет стандарта мог бы просто назвать метод .iterator(), но что если в вашем уже существующем коде нашлись бы объекты с методами .iterator(), это была бы неприятная ситуация. Так что в стандарте применяется символ вместо строки в качестве имени метода.

Символы появились в ES6, и я расскажу вам всё о них в — вы правильно угадали — в одной из следующих статей. Пока что всё, что вам нужно знать, — это то, что в стандарте может появиться новый символ, вроде Symbol.iterator, и гарантированно не будет конфликта с существующим кодом. Правда, придётся смириться с тем, что синтаксис слегка странный. Но это не такая большая цена за такую мощную новую фичу и отличную обратную совместимость.

Объект, у которого есть метод [Symbol.iterator]() называется итерируемым. В ближайшие недели мы рассмотрим, как итерируемые объекты используются во всём языке, не только в for-of, но и в конструкторах Map и Set, деструктурирующем присваивании и в новом операторе распространения (spread operator — прим. перев.).

Объекты-итераторы

Есть вероятность, что вам никогда не придётся писать свой собственный итератор с нуля. На следующей неделе мы узнаем, почему. Но для полноты давайте всё же взглянем на то, как объекты-итераторы выглядят. (Если не станете читать этот раздел, то вы пропустите в основном сухие технические подробности.)

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

var zeroesForeverIterator = {
    [Symbol.iterator]: function () {
        return this;
    },
    next: function () {
        return {done: false, value: 0};
    }
};

Всякий раз, как метод .next() вызывается, он возвращает один и тот же результат, говоря циклу for-of, что: (а.) мы ещё не закончили с итерированием, (б.) следующее значение — 0. Это означает, что for (value of zeroesForeverIterator) {} будет бесконечным циклом. Разумеется, типичный итератор не будет таким тривиальным.

Такой подход к итераторам, со свойствами .done и .value, внешне отличается от того, как работают итераторы в других языках. В Java у итераторов есть отдельные методы .hasNext() и .next(). В Python есть только один метод .next(), который бросает исключение StopIteration, когда значения заканчиваются. Но все эти три подхода принципиально одинаковы и возвращают одну и ту же информацию.

В итераторе могут быть также реализованы необязательные методы .return() и .throw(exc). Цикл for-of вызывает .return() если цикл закончился досрочно, из-за брошенного исключения или ключевых слов break или return. Реализовывать метод .return() у итератора имеет смысл, если нужно сделать очистку или освободить используемые ресурсы. Большинству итераторов реализация этого метода не понадобится. .throw(exc) — ещё более особый случай, for-of вообще никогда его не вызывает. Но мы ещё услышим о нём на следующей неделе.

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

Сначала цикл for-of:

for (ПЕРЕМЕННАЯ of ИТЕРИРУЕМЫЙ) {
    ВЫРАЖЕНИЯ
}

Вот примерный эквивалент с использованием низкоуровневых методов и нескольких временных переменных:

var $итератор = ИТЕРИРУЕМЫЙ[Symbol.iterator]();
var $результат = $итератор.next();
while (!$результат.done) {
    ПЕРЕМЕННАЯ = $результат.value;
    ВЫРАЖЕНИЯ
    $результат = $итератор.next();
}

Этот код не показывает то, как обрабатывается .return(). Мы могли бы и его добавить, но мне кажется, что это запутало бы код вместо того, чтобы его иллюстрировать. for-of легко использовать, так как много чего происходит за кулисами.

Когда можно начинать этим пользоваться?

Цикл for-of поддерживается во всех текущих версиях Firefox. Он есть в Chrome, но чтобы он стал доступен, нужно открыть chrome://flags и включить «Экспериментальный JavaScript». Он также работает в браузере Spartan (он же Edge — прим. перев.) от Microsoft, но его нет ни в одной версии Internet Explorer. Если вы хотите пользоваться этим новым синтаксисом, но вам нужно поддерживать IE или Safari, вы можете воспользоваться компилятором вроде Babel или Traceur от Google, чтобы транслировать ваш код ES6 в ES5, который поддерживают все браузеры.

На серверной стороне вам не нужен компилятор, просто используйте for-of в io.js (или Node, с флагом --harmony) уже сейчас.

(ОБНОВЛЕНИЕ: Я поначалу забыл упомянуть, что for-of по умолчанию выключен в Chrome. Спасибо Олегу за то, что указал мне на ошибку в комментариях.)

{done: true}

Фу-уф!

Ну, на сегодня это всё, но мы всё ещё не закончили с циклом for-of.

Есть ещё один вид объектов в ES6, который великолепно работает с for-of. Я не упоминал его, потому что он будет темой статьи на следующей неделе. Я считаю, что эта фича — одна из самых замечательных вещей в ES6. Если вы ещё не работали с ней в языках вроде Python и C#, возможно, поначалу она ввергнет вас в ступор. Но это самый простой способ написать итератор, эта фича полезна при рефакторинге и может изменить подход к написанию асинхронного кода, как в браузерах, так и на серверной стороне. Так что, присоединяйтесь к нам на следующей неделе и мы посмотрим на генераторы ES6 в деталях.