CSS-live.ru

ES6: Итераторы изнутри

Перевод статьи ES6 Iterators in Depth  с сайта ponyfoo.com, опубликовано на css-live.ru с разрешения автора — Николаса Беваквы.

Вот и ещё один выпуск «ES6 изнутри». Вы здесь впервые? Приветствую! Мы уже обсудили  деструктирование, литералы шаблона, стрелочные функции, оператор расширения и оставшиеся параметры, улучшения в литералах объекта, новый «сахарок» — классы поверх прототипов, и статью «let, const и “Временная мёртвая зона”». Сегодняшняя тема: итераторы.

Как и в прошлых статьях, рекомендую вам установить Babel и повторять за мной, копируя примеры с помощью интерактивной оболочки REPL, либо командной строки babel-node и файла. Это поможет гораздо лучше усвоить идеи, обсуждаемые в серии. Если вы не из тех, кто любит устанавливать что-либо на свой компьютер, то вам есть смысл залезть на CodePen и кликнуть иконку с шестерёнкой для JavaScript — у него есть препроцессор Babel, который с лёгкостью позволяет опробовать ES6. Ещё одна довольно полезная альтернатива, это использовать онлайновый REPL для Babel — он показывает скомпилированный ES5-код справа от ES6-кода, чтобы быстро сравнить.

Пока мы не начали, позвольте беззастенчиво попросить вашей поддержки, если вы наслаждаетесь моей серией «ES6 изнутри». Ваши взносы пойдут на хостинг сайту, мне на еду, на то, чтоб я не отставал от графика, и на то, чтобы Pony Foo оставался поистине источником джаваскриптовых вкусняшек.

Спасибо, что выслушали, но, пожалуй, начнём?

Протокол «Итератор» и протокол «Итерируемый»

Здесь много нового, термины тут так и переплетаются. Потерпите немного, пока я разделаюсь с объяснением некоторых из них.

В ES6 появилась пара новых протоколов, «Итераторы» и «Итерируемые». Проще говоря, о протоколах можно думать, как о соглашениях. Пока вы следуете определённому соглашению в языке, вы получаете побочный эффект. Протокол «Итерируемый» позволяет определять поведение во время итерации объектов JavaScript. Где-то глубоко в недрах интерпретаторов и в умах тех, кто настрочил спецификацию языка, есть метод @@iterator. Этот метод лежит в основе протокола «Итерируемый», и в реальности можно задействовать его с помощью так называемого «известного символа Symbol.iterator».

Мы вернёмся к символам через пару статей. Дабы не запутаться, следует знать, что метод @@iterator вызывается только один раз, когда объект должен итерироваться. Например, в начале цикла for..of (к которому мы также вернёмся немного позже) @@iterator запросит итератор. Возвращённый итератор пригодится для получения значения из объекта.

Рассмотрим следующий пример кода, это поможет нам лучше понять те идеи, на которых строится итерация. Первое, что бросается в глаза, это то, как я делаю объект итерируемым, присвоив ему таинственное свойство @@iterator с помощью свойства Symbol.iterator. Я не могу использовать символ в качестве имени свойства напрямую. Поэтому мне приходится оборачивать его в квадратные скобки, что означает, что это вычисленное имя свойства, ставшее результатом выражения Symbol.iterator — как вы, наверное, помните из статьи про литералы объекта. Объект, который возвращает метод, присвоенный свойству [Symbol.iterator], должен придерживаться протокола «Итератор». Протокол «Итератор» определяет способ получения значения из объекта, и мы должны вернуть @@iterator, который придерживается протокола «Итератор». Этот протокол указывает, что нам нужен объект с методом next. Метод next не принимает аргументы, и должен вернуть объект со двумя следующими свойствами.

  • done, если оно true, означает, что последовательность закончилась, а если false — что там могут быть еще значения
  • value — текущий элемент в последовательности

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

var foo = {
  [Symbol.iterator]: () => ({
    items: ['p', 'o', 'n', 'y', 'f', 'o', 'o'],
    next: function next () {
      return {
        done: this.items.length === 0,
        value: this.items.shift()
      }
    }
  })
}

Чтобы перебрать объект, мы могли бы использовать for..of. Как бы это выглядело? Метод итерации for..of — также новинка ES6, и он положит конец извечной битве с перебором JavaScript-коллекций, при которой то и дело под руку попадается совсем не то, что по логике должно было оказаться в итоговом наборе данных.

for (let pony of foo) {
  console.log(pony)
  // <- 'p'
  // <- 'o'
  // <- 'n'
  // <- 'y'
  // <- 'f'
  // <- 'o'
  // <- 'o'
}

For..of можно использовать для перебора любого объекта, который придерживается протокола «Итерируемый». В ES6 это массивы, любые объекты с пользовательским методом [Symbol.iterator], генераторы, коллекции узлов DOM из .querySelectorAll со товарищи, и т.д. А если хочется просто «привести» что-либо итерируемое к массиву, то есть парочка кратких альтернатив — оператор расширения и Array.from.

console.log([...foo])
// <- ['p', 'o', 'n', 'y', 'f', 'o', 'o']
console.log(Array.from(foo))
// <- ['p', 'o', 'n', 'y', 'f', 'o', 'o']

Напомним, что объект foo придерживается протокола «Итерируемый», благодаря присваиванию метода свойству [Symbol.iterator] — это можно сделать в любом месте в цепочке прототипов для foo. Это означает, что объект стал итерируемым: его можно перебрать. Указанный метод возвращает объект, который придерживается протокола «Итератор». Метод итератора вызывается только один раз, когда мы хотим начать перебор объекта, и полученный в результате итератор служит для вытаскивания значений из foo. Для перебора итерируемых объектов можно использовать for..of, оператор расширения или Array.from.

Что всё это значит?

По сути, «фишка» протоколов перебора, for..of, Array.from и оператора расширения в том, что они предоставляют выразительные и простые способы перебора коллекций и массивоподобных объектов (напр. arguments). Возможность определять то, как будет перебираться объект, великолепна, поскольку она позволяет любым библиотекам (таким, как lo-dash) прийти к чему-то единообразному, благодаря протоколу, который сам язык понимает изначально — «итерируемые объекты». Это грандиозно.

John-David Dalton, (@jdalton), твит
Цепочная обертка из lo-dash теперь итератор и итерируемый: var w = _({ a: 1, b: 2 }); Array.from(w); // => [1, 2]

Или вот ещё пример, помните, как я постоянно жаловался на объекты-обёртки в jQuery, которые не являются полноценными массивами, или на то, что document.querySelectorAll не возвращает полноценный массив? Если бы jQuery реализовала протокол «Итератор» в прототипе своих коллекций, тогда вы могли бы делать что-то вроде следующего примера.

for (let item of $('li')) {
  console.log(item)
  // <-  <li> обёрнут в объект jQuery
}

Зачем оборачивать? Потому что это выразительнее. Можете легко перебирать элементы на любом уровне вложенности.

for (let list of $('ul')) {
  for (let item of list.find('li')) {
    console.log(item)
    // <- <li> обёрнут в объект jQuery
  }
}

И здесь мы подошли к важному аспекту итерируемых объектов и итераторов

Ленивый по природе

Итераторы ленивы по своей природе. Это такая прикольная формулировка для того, что последовательность выдается по одному элементу за раз. Это может даже быть бесконечная последовательность — вполне нормальный сценарий со множеством применений. Учитывая, что итераторы ленивы, даже на объекты-обертки, в которые jQuery оборачивает каждый результат из последовательности, не уйдет сразу слишком много ресурсов. Обертки создаются не все сразу, а по одной каждый раз, когда значение вытаскивается из итератора.

Как бы выглядел бесконечный итератор? В примере ниже показан итератор с диапазоном от 1 до бесконечности (1...Infinity). Заметьте, что он никогда не вернет done: true, сообщая, что последовательность закончилась. Попытка привести итерируемый объект foo к массиву с помощью либо Array.from(foo), либо [...foo], обрушила бы нашу программу, поскольку последовательность бесконечна. Нам нужно быть осторожными с этими типами последовательностей, а то наш процесс Node или вкладка в браузере у пользователя еще взорвутся.

var foo = {
  [Symbol.iterator]: () => {
    var i = 0
    return { next: () => ({ value: ++i }) }
  }
}

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

for (let pony of foo) {
  if (pony > 10) {
    break
  }
  console.log(pony)
}

В действительности итератор не знает, что последовательность бесконечна. В этом плане он напоминает проблему остановки — невозможно узнать, бесконечна ли последовательность в коде или нет.

1266_v1

Обычно нам известно, бесконечна ли последовательность или нет, поскольку мы её создаём. Всякий раз, когда наша последовательность бесконечна, нам нужно добавить условие выхода, гарантирующее, что программа не вылетит, попытавшись перебрать все до единого значения в последовательности.

Жду вас завтра для обсуждения генераторов!

P.S. Это тоже может быть интересно:

4 комментария

  1. Спасибо за перевод этой серии статей, а то на я на английском упарился их читать уже)

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

  3. Помните, на прошлой неделе я обещал, что ES6 не сломает тот код на JS, что вы уже написали? Вот, миллионы сайтов зависят от поведения

Оставить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Получать новые комментарии по электронной почте. Вы можете подписаться без комментирования.