Рубрики
Без рубрики

Протоколы итерации JavaScript и то, как они подходят В

Итераторы являются одной из самых недооцененных особенностей ES2015, и они глубоко связаны с несколькими другими великими функциями ES2015. Давай погрузимся!. Tagged с JavaScript, ES2015, ES6, промежуточный.

Одним из самых крутых и наиболее недооцененных IMHO функции, представленные ECMascript 2015 (ES2015, он же ES6), была пара Протоколы итерации , которые определяют «итераторы» и «итерали» в JavaScript. Эти протоколы дают нам нативный способ создания пользовательских видов контейнеров, списков и псевдо-кинды-листов, и в сочетании с двумя другими функциями, представленными в ES2015, для ... из Функции петли и генератора ( функция* ), они дают нам несколько очень хороших новых способностей.

Чтобы играть с конкретным примером, давайте посмотрим, как мы можем реализовать и зацикливаться на Связанный список Три разных способа:

  • старая школа, не подвергая
  • Использование итерационных протоколов
  • используя генератор

Если вам нужно быстрое освежение в том, что такое связанный список, и вы чувствуете себя немного TL; DR о статье Wikipedia, которую я связал там, вот основы: связанный список можно рассматривать как список материалов, построенных с использованием группы из отдельно связанных узлов, каждый из которых знает только о своем собственном значении и следующей вещи в списке, с родительским объектом, который знает о начале («голова») и конец («хвост») списка. Вы добавляете в список, создавая новый узел, связывая его текущий хвост и обновляя ссылку на хвост родителей. Существует множество вариаций, таких как вдвойне связанные списки, и у них есть множество преимуществ производительности по сравнению с традиционными массивами для определенных приложений, но я не собираюсь вдаваться ни в что здесь, потому что это становится сложным быстро; Если вы не знакомы со всем этим, ознакомьтесь с статьей в Википедии и Google вокруг статей и, возможно, курсов MOOC по «структурам данных».

Вот своего рода наивная реализация связанного списка с использованием класса ES6, но не используя итераторы:

class LinkedList {
    constructor() {
        this.head = this.tail = null
    }
    push(val) {
        const next = {val, next: null}
        if (this.head === null) {
            this.head = this.tail = next
        }
        else {
            this.tail.next = next
            this.tail = next
        }
    }
    forEach(fn) {
        let curr = this.head
        while (curr !== null) {
            fn(curr.val)
            curr = curr.next
        }
    }
}

// example
const l = new LinkedList
l.push(10)
l.push(20)
l.push(30)
l.forEach(n => console.log(n))

Хорошо, давайте разберем это.

Когда LinkedList впервые инициализируется в Constructor () , в нем ничего нет, так что это голова и хвост Свойства оба установлены на NULL Анкет

push () Метод добавляет новый элемент в список. Каждый раз push () называется, новый объект создается для сохранения вновь добавленного значения, с двумя свойствами:

  • A val собственность для удержания стоимости, переданной в
  • A Следующий свойство, чтобы указать на следующий узел в списке

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

Мы объявляем этот новый узел новым хвостовым узлом списка в двух шагах:

  • установить Следующий Свойство текущего списка хвост к новому узлу
  • установить хвост свойство списка в новый узел

Там также есть немного дополнительного шага, чтобы проверить, есть ли голова нулевой, чтобы справиться с самым первым вызовом push () , и я уверен, что этот класс может быть рефактован, чтобы избежать повторной проверки, но это всего лишь игрушечный пример, поэтому пока игнорируйте неэффективность.

Теперь важная часть: foreach () метод Здесь мы перечитываем содержимое связанного списка. Мы не можем использовать традиционный для (let; i петля, чтобы итерация над узлами, так как у нас нет прямых (известных "случайных") доступа к каким -либо узлам, кроме голова и текущий хвост . Вместо этого нам нужно начать с голова Узел и пройдите по списку по одному узлу за раз, используя Следующий Свойство текущего узла на каждом шаге, чтобы найти следующий узел, пока мы не нажмем NULL Анкет Теперь я решил написать это как В то время как Цикл, потому что я думаю, что его легче читать, но это может быть написано как для Вместо этого петля:

forEach(fn) {
    for (let curr=this.head; curr !== null; curr=curr.next) {
        fn(curr.val)
    }
}

Выбирайте, они эквивалентны.

Теперь этот код не так уж и плох, но любой код, который хочет использовать ваш класс, должен будет использовать foreach Метод вместо более хорошей конструкции, такой как для ... из петля. Это может сделать его менее совместимым с другими типами данных, такими как массивы. Если бы вы писали какой -то сложный код обработки на основе массивов, но через некоторое время поняли, что вы находитесь в обстоятельствах, когда вы действительно должны использовать связанный список, может быть отзыве обнаружить, что вам нужно вернуться и переписать кучу Код, который использует для ... из петли, чтобы переключиться, и вы можете решить, что у вас нет времени. Это может показаться глупым примером, и, конечно, это преднамеренно упрощенный футляр для игрушек, но, как правило, кросс-композиция-это хорошо.

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

Во -первых, давайте возьмем удар и поговорим о том, что это за протоколы. Их два: Протокол итератора и итерабильный протокол . Оба довольно просты, поэтому нам там повезло.

Итераторы

Протокол итератора более интересный. Чтобы объект был квалифицирован как «итератор», ему нужно только одно: a Next () метод Каждый раз Next () вызывается, он должен вернуть объект с двумя свойствами: значение , представляя следующее значение, чтобы быть имерным, и Готово , указывая, осталась ли другая итерация.

Конкретно, на каждом вызове, если есть хотя бы одно значение, которое остается для итерации, функция должна вернуть объект, как это:

{ value: 'next value here', done: false }

Если нечего производить, функция должна вернуть объект, как это:

{ value: undefined, done: true }

Я покажу вам какой -нибудь пример кода через минуту. Но сначала нам нужно поговорить о …

Иеры

Итерабильный протокол еще проще, чем протокол итератора. Концептуально, итерабильный – это любой объект, который может производить итератор при необходимости. Технически говоря, объект считается итерабильным, если он имеет метод с особым именем (удерживая SEC), который при вызове возвращает итератор, как определено выше.

Теперь об этом особом имени. Другой недооцененной особенностью ES2015 стало введение нового примитивного типа, Символ Анкет Здесь есть о чем поговорить, но давно рассчитанные, символы могут использоваться в качестве глобально уникальных ключей объекта, чтобы все говорят об одном и том же, и не две разные идеи с одним и тем же именем. (Есть гораздо больше, чтобы поговорить с символами, и я ES6 Глубина: символы , а также остальная часть ES6 Глубокая серия, на самом деле.)

Дело для нас в том, что существует несколько встроенных специфических символов, используемых для реализации протоколов, таких как итерабильный протокол, который использует глобальный ключ Symbol.iterator Чтобы определить метод, который возвращает итератор. Вот тривиальный класс, который создает итерабильный, чтобы зацикливаться на ARG, переданных конструктору:

class ArgsIterable {
    constructor(...args) {
        this.list = args
    }
    [Symbol.iterator]() {
        const list = this.list
        let i=-1
        return {
            next() {
                i += 1
                if (i

Так как же это работает? Давайте пройдемся через это:

const iterable = new ArgsIterable(1,3,5,7)
const iterator = iterable[Symbol.iterator]()
console.log(iterator.next())
console.log(iterator.next())
console.log(iterator.next())
console.log(iterator.next())
console.log(iterator.next())
console.log(iterator.next())
/* output:
{value: 1, done: false}
{value: 3, done: false}
{value: 5, done: false}
{value: 7, done: false}
{done: true}
{done: true}
*/

Первые 4 раза iterator.next () называется, мы получаем ценность в массиве, и нам сказали, что мы еще не достигли конца. Затем, как только мы достигнем конца, мы начинаем всегда отправлять {Dode: true} Анкет

Ключевым преимуществом этого подхода является то, что для ... из Loop понимает этот протокол:

for (const n of new ArgsIterable(1,3,5,7)) {
    console.log(n)
}
/* output:
1
3
5
7
*/

Если это кажется большой работой, вы не ошибаетесь, но есть решение: генераторы. Но мы доберемся до этого через минуту. А пока давайте вернемся к нашему LinkedList учебный класс.

Теперь, когда мы понимаем, как работают итераторы и иеры, давайте превратим наш класс в итерабильный.

class LinkedList {
    constructor() {
        this.head = this.tail = null
    }
    push(val) {
        const next = {val, next: null}
        if (this.head === null) {
            this.head = this.tail = next
        }
        else {
            this.tail.next = next
            this.tail = next
        }
    }
    [Symbol.iterator]() {
        let curr = this.head
        return {
            next() {
                if (curr === null) {
                    return { done: true }
                }
                else {
                    const next = { value: curr.val, done: false }
                    curr = curr.next
                    return next
                }
            }
        }
    }
}

// example
const l = new LinkedList
l.push(10)
l.push(20)
l.push(30)
for (const n of l) console.log(n)
/* output:
10
20
30
*/

Не слишком ужасно, верно? [Symbol.iterator] () Возвращает объект с Next () Метод, с локальной переменной Curr Чтобы отслеживать текущий узел, точно так же, как мы имели в нашем foreach () Метод ранее. Каждый раз Next () вызывается, мы проверяем, Curr это нулевой . Если это так, мы сообщили звонящему, что мы закончили; Если нет, мы готовим наш объект ответа, переместите карт Один узел вниз по списку, чтобы подготовиться к следующей итерации, затем верните наш объект ответа. Своего рода менее контролирующая версия foreach () , где пользователь может взять следующий элемент в списке всякий раз, когда он готов. И если вы запустите пример кода в конце, вы увидите эти экземпляры нашего LinkedList учебный класс Просто работа с для ... из Петли сейчас! Как это круто?

Если вы не убеждены, позвольте мне показать вам очень хороший перк, который бесплатно приходит бесплатно, когда вы реализуете итерабильный протокол: распространяется в массив с оператором спреда ES2015! Если вам нужно использовать связанный список для вашей основной обработки, но вы хотите массив с результатами, возможно, чтобы запустить некоторые методы массива, вам повезло! Просто распространите свой LinkedList экземпляр в массив:

const list = new LinkedList
list.push(10)
list.push(20)
list.push(30)
list.push(50)
// magic!
const final = [...list].map(n => n*2).filter(n => n%3 === 0)[0]
console.log(final)
// output: 60

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

Как я упоминал выше, это все еще может показаться большим умственным усилием и кодом без большой пользы. Но, как я уже упоминал, есть решение:

Еще одна из моих любимых функций ES2015, генераторы часто упоминаются в учебных пособиях как «приостановленные функции». Это довольно интуитивно понятный способ подумать о них, но я бы немного приспособился: я бы предпочел назвать их приостановленными итерами. Давайте посмотрим на простой пример, тогда я объясню:

function* countTo(n) {
    for (let i=1; i<=n; i++)
        yield i
}

// example
for (const n of countTo(5))
    console.log(n)
/* output:
1
2
3
4
5
*/

Как вы уже догадались, ключом здесь является урожай ключевое слово. В первый раз через для ... из Цикл, функция генератора работает сверху до тех пор, пока не достигнет этого урожай i , в этот момент он возвращает значение я (Сорта; нести меня) и «пауза» там функции, отсюда и дескриптор «приостановленного». В следующий раз через петлю он поднимается прямо там, где он остановился, и продолжается, пока не попадет в другой урожай , когда он снова останавливается. Это продолжается до тех пор, пока функция не достигнет урожай , но вместо этого достигает возврат утверждение или, в нашем случае, конец функции. Но как именно это связано с этим с для ... из петля? Разве этот цикл не ожидает итерабильного?

Если вы позвоните граф (5) Прямо и посмотрите на результат, вы увидите что -то очень интересное. Вот что я получаю, когда кону немного в инструментах Dev Chrome:

> x = countTo(5)
  countTo {}
> x.next
  f next() { [native code] }
> x[Symbol.iterator]
  f [Symbol.iterator]() { [native code] }

Здесь важна то, что вызов генератора не возвращает значение напрямую: он возвращает объект, который двигатель описывает как «приостановленный», что означает, что код функции генератора еще не запускается. Интересно, что объект имеет оба Next () Метод и A [Symbol.iterator] метод Другими словами, он возвращает объект, который является оба итерационным и И итератор!

Это означает, что генераторы могут использоваться как в качестве отдельных генераторов последовательностей, таких как countto (n) Метод выше, и как Действительно просто Способ сделать ваш объект итератным!

Давайте снова вернемся к нашему LinkedList учебный класс а также Замените наш обычай [Symbol.iterator] Метод с генератором:

class LinkedList {
    constructor() {
        this.head = this.tail = null
    }
    push(val) {
        const next = {val, next: null}
        if (this.head === null) {
            this.head = this.tail = next
        }
        else {
            this.tail.next = next
            this.tail = next
        }
    }
    *[Symbol.iterator]() {
        let curr = this.head
        while (curr !== null) {
            yield curr.val
            curr = curr.next
        }
    }
}

// example
const l = new LinkedList
l.push(10)
l.push(20)
l.push(30)
for (const n of l) console.log(n)
/* output:
10
20
30
*/

Две вещи о [Symbol.iterator] метод Во -первых, обратите внимание, что нам пришлось прикрепить звездочку на передней части его, чтобы указать, что это функция генератора. Во -вторых, и самое главное, посмотрите на тело метода: это выглядит знакомо? Это почти тот же код, что и foreach () Метод из более раннего, просто заменив обратный вызов с урожай ключевое слово!

Поскольку генератор возвращает объект, который реализует протокол итератора, генераторы делают его Так просто Чтобы сделать свой объект итератным! Вы можете использовать всевозможные интересные шаблоны хранения и алгоритмы обхода, и это не имеет значения: генераторы делают это легко!

Возможно, более конкретный пример, я бы хотел поговорить о холсте. Я лично люблю возиться с манипуляциями с изображениями, используя элемент холста HTML5. Вы можете загрузить изображение, используя собственное Изображение Объект, затем нарисуйте его на холст, возьмите его Imagedata объект и непосредственно манипулировать значениями пикселей. Но есть улов с Imagedata : Это необработанные данные пикселя, хранящиеся компьютером, что означает, что вместо того, чтобы храниться как массив пикселей, что -то вроде: [{r: 255, b: 128, g: 0, a: 255}, ...] , это один длинный, плоский массив байтов, например: [255, 128, 0, 255, ...] . Это означает, что для того, чтобы зацикливаться на пикселях, вам обычно нужно сделать что -то вроде этого:

for (let i=0; i

Это … хорошо , Но это раздражает неоднократно писать, если вам нужно сделать это кучу, и это довольно странно, как функция UTIL, которая проводит обратный вызов:

function processPixels(imgData, processPixel)
    for (let i=0; i

Обратные вызовы … брутто 😢

Другой вариант – зацикливаться на Imagedata Сначала буферизируйте и преобразуйте его в массив, затем используйте для ... из Переверните по массиву, чтобы сделать его более читабельным, но, учитывая, насколько большие изображения в наши дни, это Огромный трата памяти.

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

function* getPixels(imgData) {
    for (let i=0; i

Чисто и просто!

Что меня больше всего поразило о спецификации ES2015, даже в том, что сами красивые новые функции сами, так это то, сколько размышлений зашла в создание функций, которые работали вместе Действительно приятными способами сделать JavaScript глубоко сплоченным языком. Синтаксис класса, протокол итерации, для ... из Петли, генераторы, символы и оператор разброса массива – все это функции, которые были добавлены в ES2015, и все они так плавно сочетаются друг с другом. Это действительно впечатляющий подвиг, и он стал лучше с ES2016-2018. Я был очень впечатлен процессом предложения TC39 и функциями, которые были из него. Я надеюсь, что это останется таким образом! Это такие функции, которые заставляют меня к тому, чтобы я был в будущем JavaScript и Интернете.

Оригинал: “https://dev.to/kenbellows/the-javascript-iteration-protocols-and-how-they-fit-in-1g8i”