От автора: в этой статье мы рассмотрим различные способы того, как создаются в JavaScript итераторы и итерируемые значения: в частности, функции, итераторы, итерируемые объекты и генераторы.
JavaScript — это очень гибкий язык, и чаще всего вы можете достичь одних и тех же целей разными способами, итераторы не являются исключением!
Википедия определяет итераторы следующим образом: В компьютерном программировании итератор — это объект, который позволяет программисту перебирать контейнер, особенно списки. Различные типы итераторов часто предоставляются через интерфейс контейнера.
Мы расширим это определение, поскольку не будем концентрироваться на построении итераторов для предварительно вычисленных значений, таких как списки, но рассмотрим, как выполнять итерации для генерируемых последовательностей, таких как последовательность Фибоначчи.
Скорее всего, вы не будете использовать последовательность Фибоначчи в повседневном программировании (если вы не проходите собеседование в какой-либо компании, которая хочет проверить ваши знания рекурсии), но идея генерирования последовательности значений по требованию (отложенная оценка) подходит для многих реальных сценариев, таких как:
обход пользовательских структур данных
потребляемые API-интерфейсы с нумерацией страниц
обработка очереди
обработка построчно длинных файлов
считывание всех записей из таблицы SQL
и т.п.
Последовательность Фибоначчи
Если вы никогда ранее не видели последовательность Фибоначчи (или вы не помните точное определение), вот как она выглядит: 1 1 2 3 5 8 13 21 …
По сути, каждое следующее число в последовательности представляет собой сумму двух предыдущих чисел. В более формальных математических терминах вы можете определить последовательность как:
F1 = F2 = 1 Fn = F(n-1) + F(n-2)
Несколько моментов, которые стоит отметить:
Последовательность бесконечна (было бы невозможно сохранить ее в списке без верхнего предела).
Она состоит из положительных целых чисел.
Итак, как происходит создание кода JavaScript, который позволяет нам перебирать эту последовательность и вычислять произвольное количество элементов? Ну, есть много способов …
Функции
В JavaScript функции являются гражданами первого класса, и большинство шаблонов можно моделировать с использованием только простых функций. Это станет естественным для вас, когда вы овладеете понятиями области действия функций, анонимных функций и вложенных функций.
Итак, как мы можем построить последовательность Фибоначчи, используя только функции? Вот пример:
const genFib = (max = Number.MAX_SAFE_INTEGER) => { // инициализируем значения по умолчанию в диапазоне let n1 = 0 let n2 = 0 // возвращаем анонимную функцию, которая возвращает следующий элемент // при каждом вызове return () => { // вычисляем следующее значение const nextVal = n2 === 0 ? 1 : n1 + n2 // переопределяем n1 и n2 to в соответствии с новыми значениями const prevVal = n2 n2 = nextVal n1 = prevVal // если мы достигли верхнего предела (итерация завершена), возвращаем ноль if (nextVal >= max) { return null } // возвращаем новое значение return nextVal } }
Я добавил несколько комментариев, чтобы облегчить понимание кода, но давайте пройдемся по нему еще раз.
genFib является функцией, принимающей необязательный параметр, который является верхней границей, используемой, чтобы определить, когда остановить вычисление элементов в последовательности. Числа JavaScript начинают терять точность после Number.MAX_SAFE_INTEGER, так что это разумное значение по умолчанию.
Первое, что происходит в функции — это инициализация области действия функции. n1и n2 являются единственными значениями, которые нам нужны для вычисления элемента последовательности. Они представляют последние 2 вычисляемых числа. Мы устанавливаем для них по умолчанию 0.
В этот момент функция возвращает анонимную функцию. Эта функция может быть вызвана произвольное количество раз, и каждый раз она будет вычислять и возвращать новый элемент в последовательности, обеспечивая соответствующее обновление внутреннего состояния.
Обратите внимание, что genFib будет инициирована новая изолированная область, содержащая n1и n2. Эти значения будут доступны (и могут быть изменены) только анонимной функции, возвращаемой genFib. Это означает, что вы можете генерировать несколько «итераторов», и каждый из них будет независимым друг от друга.
Чтобы понять это еще лучше, давайте рассмотрим пример того, как пользователь будет использовать этот код:
const f = genFib(6) // лимит последовательности для чисел меньше 6 f() // 1 f() // 1 f() // 2 f() // 3 f() // 5 f() // null f() // null f() // null // или с помощью цикла // выводим все числа последовательности меньше MAX_SAFE_INTEGER const f2 = genFib() let current while ((current = f2()) !== null) { console.log(current) }
Протокол Iterator
В предыдущем примере мы придумали собственный способ определить, как перебирать элементы (возвращенная анонимная функция) и как понять, была ли последовательность закончена (возврат null).
ECMAScript 2015 предоставляет стандартный и совместимый способ определения объектов итераторов. Это называется протоколом Iterator.
Проще говоря, объект JavaScript является итератором, если он реализует метод next() со следующей семантикой:
next() не принимает никаких аргументов.
next() должен вернуть объект с 2 свойствами: done и value.
done является логическим значением, и для него будет установлено true в том и только в том случае, если в последовательности больше нет элементов.
value будет содержать фактическое значение, вычисленное в последней итерации (может быть undefined, когда done равно true).
Хорошо, теперь давайте перепишем нашу последовательность Фибоначчи для реализации протокола Iterator:
const genFibIterator = (max = Number.MAX_SAFE_INTEGER) => { let n1 = 0 let n2 = 0 // на этот раз мы возвращаем объект итератора (вместо функции) return { // логика, которая нужна для вычисления следующего элемента находится в методе `next` next: () => { // вычисляем следующее значение let nextVal = n2 === 0 ? 1 : n1 + n2 // переопределяем n1 и n2 в соответствии с новыми значениями const prevVal = n2 n2 = nextVal n1 = prevVal // если мы достигли верхнего предела (итерация завершена), // задаем для done - true, а для nextVal - undefined let done = false if (nextVal >= max) { nextVal = undefined done = true } // возвращаем объект итератора в качестве протокола итерации return { value: nextVal, done } } } }
Комментарии в коде должны помочь вам понять новую логику. Давайте посмотрим, как использовать нашу новую реализацию итератора Фибоначчи:
const it = genFibIterator(6) // { next: [Function: next] } it.next() // { value: 1, done: false } it.next() // { value: 1, done: false } it.next() // { value: 2, done: false } it.next() // { value: 3, done: false } it.next() // { value: 5, done: false } it.next() // { done: true } // или const it2 = genFibIterator(6) let result = it2.next() while (!result.done) { console.log(result.value) result = it2.next() } // 1 // 1 // 2 // 3 // 5
Протокол Iterable
В предыдущем разделе мы увидели, как определять объекты Iterator, которые соответствуют протоколу Iterator. В действительности, мы могли бы захотеть выразить понятие «итеративность» в более общем виде, чтобы для любого объекта мы могли сказать, является ли такой объект итеративным или нет.
По этой причине ECMAScript 2015 определяет также протокол Iterable. Объект называется итеративным, если он предоставляет вызываемое свойство Symbol.iterator, которое является функцией, возвращающей объект итератора. Вы можете интроспективно проверить, является ли объект итеративным, с помощью следующего кода:
function isIterable(obj) { return Boolean(obj) && typeof obj[Symbol.iterator] === 'function' }
ECMAScript 2015 также предоставляет новую конструкцию for ( for…of), которая позволяет легко перебирать элементы итерируемого объекта:
for (let current of someIterable) { console.log(current) }
Итерируемые объекты также можно использовать в сочетании с оператором распространения для быстрой загрузки всех значений и сохранения их в массиве:
const allValues = [...someIterable]
Хорошо, теперь давайте перепишем нашу последовательность Фибоначчи для реализации протокола Iterable:
const genFibIterable = (max = Number.MAX_SAFE_INTEGER) => { let n1 = 0 let n2 = 0 // возвращаем объект iterable return { [Symbol.iterator] () { // возвращаем итератор return { next() { let nextVal = n2 === 0 ? 1 : n1 + n2 const prevVal = n2 n2 = nextVal n1 = prevVal let done = false if (nextVal >= max) { nextVal = undefined done = true } return { value: nextVal, done } } } } } }
Здесь мы просто переместили реализацию итератора, описанную в предыдущем разделе, в функцию Symbol.iterator. Обратите внимание, что можно предложить реализацию, которая будет одновременно удовлетворять протоколам Iterator и Iterable:
const genFib = (max = Number.MAX_SAFE_INTEGER) => { let n1 = 0 let n2 = 0 return { // сначала удовлетворяем протоколу Iterator next: () => { let nextVal = n2 === 0 ? 1 : n1 + n2 const prevVal = n2 n2 = nextVal n1 = prevVal let done = false if (nextVal >= max) { nextVal = undefined done = true } return { value: nextVal, done } }, // это удовлетворяет протоколу Iterable [Symbol.iterator] () { // возвращаем `this`, потому что сам объект и является итератором return this } } }
Комментарии в коде должны помочь вам понять логику этих двух реализаций. С помощью этих новых подходов вы можете генерировать числа из последовательности Фибоначчи следующим образом:
// выводим все числа в последовательности меньше MAX_SAFE_INTEGER const f = genFibIterable() for (let n of f) { console.log(n) } // создаем массив со всеми числами Фибоначчи меньше 17 const f2 = genFibIterable(17) const lowerThan17 = [...f2] // [ 1, 1, 2, 3, 5, 8, 13 ]
Если в этот момент вы все еще пытаетесь понять логическое различие между итератором и итерируемым объектом, вы можете представить это следующим образом:
iterable является объектом, который вы можете итерировать.
iterator — это объект курсора , который позволяет перебирать iterable.
Генераторы
Еще одно замечательное дополнение, появившееся в ECMAScript 2015 для JavaScript — это Генераторы. Более конкретно, ECMAScript 2015 определяет функцию генератора и объекты генератора.
Объявление function* (ключевое слово function со звездочкой) определяет функцию генератора, которая возвращает объект генератора.
Генераторы — это функции, которые можно завершить, а затем снова ввести. Их контекст (привязка переменных) будет сохранен при повторном вхождении.
Чтобы немного упростить эту концепцию, вы можете представить функции генератора как функции, которые могут «возвращать» (или «выдавать») несколько раз. Давайте рассмотрим синтаксис генератора на простом примере:
// функция генератора function* countTo3() { yield 1 yield 2 return 3 }
В этом примере мы определяем counter, который генерирует числа от 1 до 3. Мы можем использовать его следующим образом:
// c - это объект генератора const c = countTo3() c.next() // { value: 1, done: false } c.next() // { value: 2, done: false } c.next() // { value: 3, done: true } c.next() // { done: true } c.next() // { done: true } // ...
Итак, генератор работает следующим образом:
При вызове функции генератора , возвращается объект генератора.
У объектов генератора есть метод next().
Когда вы вызываете метод next() объекта генератора, код генератора будет выполняться до тех пор, пока не встретится первый yield (или return).
Если найден yield, код останавливается, и выданное значение будет передано в вызывающий контекст через объект со следующей формой: { value: yieldedValue, done: false }.
При следующем вызове next() выполнение будет возобновлено с того места, где оно было первоначально приостановлено, до появления нового найденного yield или return.
Если найден оператор return (или функция завершена), возвращаемый объект будет выглядеть так: { value: returnedValue, done: true } (обратите внимание, что для done сейчас установлено значение true).
Когда генератор завершит работу next(), всегда будут выполняться последовательные вызовы { done: true }.
Конечно, причина, по которой мы рассматриваем эту тему, заключается в том, что мы можем реализовать последовательность Фибоначчи как генератор:
function* Fib (max = Number.MAX_SAFE_INTEGER) { // инициализируем переменные состояния let n1 = 0 let n2 = 0 // теперь мы можем предварительно инициализировать nextVal со значением 1, как часть состояния let nextVal = 1 // выполняем цикл, пока не достигнем максимального числа while (nextVal <= max) { // выдаем текущее значение yield nextVal // смещаем nextVal -> n2 и n2 -> n1 const prevVal = n2 n2 = nextVal n1 = prevVal // вычисляем следующее значение nextVal = n1 + n2 } }
Комментарии в коде должны помочь вам понять эту реализацию. Вы можете сразу заметить, что, поскольку нам не приходится иметь дело с вложенной функцией, реализация кажется более легкой для чтения, или, по крайней мере, может казаться, что код легче читать и легче понимать фактический поток выполнения.
По этой причине вы можете предпочесть использовать в подобных сценариях генераторы, а не простые функции. Мы можем использовать нашу новую последовательность Фибоначчи на основе генератора, как в этом примере:
const fib = Fib(6) fib.next() // { value: 1, done: false } fib.next() // { value: 1, done: false } fib.next() // { value: 2, done: false } fib.next() // { value: 3, done: false } fib.next() // { value: 5, done: false } fib.next() // { done: true } // или const fib2 = Fib(6) let result = fib2.next() while (!result.done) { console.log(result.value) result = fib2.next() } // 1 // 1 // 2 // 3 // 5
В этот момент вы можете спросить: Является ли объект-генератор итератором или итераторируемым? Что ж, получается, что объект-генератор является итератором и итераторируемым!
Таким образом, вы можете также использовать нашу последнюю реализацию с for…of и расширенным синтаксисом:
const fib = Fib(6) for (let current of fib) { console.log(current) } // 1 // 1 // 2 // 3 // 5 // or const fib2 = Fib(6) [...fib2] // [ 1 1 2 3 5 ]
Наконец, поскольку генераторы являются итераторами , вы можете использовать их как свойство Symbol.iterator итерируемого объекта . Это может помочь вам определить логику итерации более элегантно и кратко, используя ключевое слово yield.
В некоторой смысле вы можете представить генераторы как синтаксический сахар для определения итерируемых объектов.
Заключение
В этой статье мы узнали о различных способах генерации динамических последовательностей с использованием простых функций, итераторов, итерируемых объектов и генераторов.
Обратите внимание, что эти подходы идеальны, когда операция, необходимая для генерации следующего элемента, является синхронной (для нее не требуется асинхронная загрузка внешних ресурсов).
Когда вам приходится перебирать значения, которые становятся асинхронными, вы должны полагаться на разные шаблоны, такие как эммитеры событий, потоки или асинхронные итераторы.
Также обратите внимание, что у генераторов есть некоторые интересные расширенные функции, не описанные в этой статье, такие как возможность передавать новые значения в контекст каждый раз, когда вызывается .next(), или вводить исключения. Поэтому обязательно ознакомьтесь с документацией по генераторам.
Автор: Luciano Mammino
Источник: https://loige.co/
Редакция: Команда webformyself.