Skip to content

generators and iterators

garevna edited this page Nov 4, 2019 · 31 revisions

🎓 Генераторы и итераторы

ES6

🎓 Генератор

Функция-генератор объявляется с помощью ключевого слова function*

⚠️ * - обязательный атрибут функции-генератора

Функция-генератор определяет порядок ( протокол ) итерирования структуры данных

Сама по себе функция-генератор ничего не итерирует

С помощью функции-генератора создается объект-итератор

let iterator = generator ( ... )

Она ничего не возвращает, кроме, как уже было сказано, объекта-итератора с заложенным в нем протоколом перебора значений

⚠️ Именно поэтому генератор вместо оператора return использует оператор yield

function* generator ( ... ) {
    ...
    yield ...
}

Оператор yield позволяет управлять работой итератора

Этим оператором функция-генератор говорит итератору, что в этом месте нужно остановиться и вернуть текущее значение


🎓 next()

У объекта-итератора есть обязательный метод next()

С помощью этого метода итератор переходит от текущего элемента структуры данных к следующему

⚠️ Этот метод возвращает объект с двумя свойствами: value и done

  • Свойство value содержит то, что указано в протоколе генератора после ключевого слова слова yield
  • Свойство done принимает значение true, когда процесс итерирования структуры данных завершен

☕ 1
function* colorsGenerator () {
    while ( true ) {
        yield `rgb(
            ${Math.round ( Math.random() * 255 )},
            ${Math.round ( Math.random() * 255 )},
            ${Math.round ( Math.random() * 255 )}
        )`
    }
}

let colorIterator = colorsGenerator ()

for ( var x=0; x < 100; x++ ) {
    let point = document.body.appendChild (
        document.createElement ( 'div' )
    )
    point.style = `
        float: left;
        width: 10px;
        height: 10px;
        background-color: ${ colorIterator.next().value};
    `
}

Используя IIFE, можно сократь код:

const colorIterator = ( function* () {
    while ( true ) {
        yield `rgb(
            ${Math.round ( Math.random() * 255 )},
            ${Math.round ( Math.random() * 255 )},
            ${Math.round ( Math.random() * 255 )}
        )`
    }
})()

Также можно отдельно вынести в функцию код создания элемента с параметрами ширины и высоты:

function createColoredElement ( w, h ) {
    let point = document.createElement ( 'div' )
    point.style = `
        position: absolute;
        width: ${w}px;
        height: ${h}px;
        background-color: ${ colorIterator.next().value};
    `
    return point
}

После чего можно в цикле создавать элементы:

for ( var x = 0; x < 75; x++ ) {
    document.body.appendChild (
        createColoredElement ( 400-x*5, 400-x*5 )
    )
}

☕ 2

Для бесконечной подзагрузки данных с сервера при прокрутке страницы можно также использовать итератор:

const iterator = (
    function* ( arg ) {
        while ( true ) {
            yield fetch ( `https://api.github.com/users?since=${arg}` )
                .then (
                    response => response.json()
                        .then (
                            response => response.forEach (
                                user => {
                                    let img = document.body.appendChild (
                                        document.createElement ( "img" )
                                    )
                                    img.src = user.avatar_url
                                    img.height = 100
                                }
                            )

                        )
                )
            arg += 30
        }
    }
)(0)


document.body.onmousewheel = function ( event ) {
    iterator.next()
}

☕ 3

Пусть есть некий объект user

let user = {
    login: "Сергей",
    avatar: "https://www.shareicon.net/data/2015/12/14/207817_face_300x300.png",
    email: "serg789@gmail.com",
    place ( tagName ) {
        return document.body.appendChild (
            document.createElement ( tagName )
        )
    },
    showAvatar () {
        let ava = this.place ( "img" )
        ava.src = this.avatar
        ava.width = "70"
        return ava
    },
    showLogin () {
        let x = this.place ( "h3" )
            .innerHTML = this.login
        return x
    },
    showEmail () {
        let x = this.place ( "p" )
            .innerHTML = this.email
        return x
    }
}

С помощью генератора определим протокол итерирования этого объекта:

user.generator = function* () {
    yield this.showLogin ()
    yield this.showEmail ()
    yield this.showAvatar ()
}

Теперь создадим объект итератора:

user.iterator = user.generator ()

и запустим цикл итерирования:

while ( !user.iterator.next().done ) {}

На самом деле такое решение является чрезмерно громоздким

Все значительно упростится с использованием глобального символа Symbol.iterator


🎓 Symbol.iterator

Все очень просто:

Если у объекта есть свойство Symbol.iterator, то этот объект является итерабельным

( то есть можно перебирать его свойства оператором for...of )

Symbol.iterator является ссылкой на функцию-генератор


Используем Symbol.iterator в контексте предыдущего примера

user [ Symbol.iterator ] = function* () {
    yield this.showLogin ()
    yield this.showEmail ()
    yield this.showAvatar ()
}

Теперь объект user можно итерировать обычным for...of

for ( var x of user ) {}

или воспользоваться оператором spread:

console.log ( ...user )

☕ 4
const elements = [
    { tagName: "div", attrs: { id: "first", innerText: "first" } },
    { tagName: "article", attrs: { id: "second", innerText: "second" } },
    { tagName: "figure", attrs: { id: "third", innerText: "third" } },
    { tagName: "p", attrs: { id: "forth", innerText: "forth" } }
]

elements [ Symbol.iterator ] = function* () {
    let itemNum = 0
    while ( itemNum < this.length ) {
        yield ( () => {
            var elem = document.body.appendChild ( 
                document.createElement (
                    this [ itemNum ].tagName
                )
            )
            if ( this [ itemNum ].attrs ) 
                for ( var x in this [ itemNum ].attrs ) {
                    elem [ x ] = this [ itemNum ].attrs [ x ]
                }
            itemNum++
            return elem
        })()
    }
}

for ( let elem of elements ) {}

🎓 Асинхронный генератор

☕ 5

Создадим генератор, который выдает по одному символу в секунду из массива, переданного ему в качестве аргумента

async function* messageGenerator ( arr ) {
    while ( arr.length > 0 ) {
        let result = await new Promise (
            function ( resolve ) {
                setTimeout (
                    () => resolve ( arr.shift() ),
                    1000
                )
            }
        )
        yield result
    }
}

Поскольку протокол итерирования, заложенный в генераторе, возвращает промис на каждой итерации, для работы с ним объявим асинхронную функцию showMessage

showMessage создаст итератор с помощью генератора messageGenerator, передав ему строку, которая будет выводиться на страницу по одному символу в секунду

showMessage будет ждать ( await ), когда асинхронный итератор вернет очередное значение, и после этого выведет его на страницу

async function showMessage ( message ) {
    const iterator = messageGenerator ( [...message] )
    let finish = false

    while ( !finish ) {
        let currentState = await iterator.next()
        document.body.innerText += !currentState.done ?
            currentState.value : ""
        finish = currentState.done
    }
}

Вызовем асинхронную функцию showMessage:

showMessage ( "Привет, студент!" )

☕ 6
Асинхронный генератор
let circle = document.createElement ( "div" )
circle.style = `
    border: solid 2px blue;
    width: 50px;
    height: 50px;
    position: absolute;
    border-radius: 50%;
    transition: all 0.2s;
    opacity: 1;
`

circle.bubblesGenerator = ( async function* () {
    let bubble = () => new Promise (
        function ( resolve ) {
            setTimeout ( () => resolve ( "next" ), 100 )
        }
    )
    while ( true ) {
        let radius = this.offsetWidth > 200 ?
                     50 : this.offsetWidth + 5
        await bubble ()
        this.style.width = `${radius}px`
        this.style.height = `${radius}px`
        this.style.opacity = radius === 50 ?
            1 : Math.max ( this.style.opacity - 0.02, 0 )
        yield radius
    }
}).call ( circle )


document.body.appendChild ( circle )

async function show () {
    let step = 200
    while ( step --> 0 )
        await circle.bubblesGenerator.next()
}

show()

☕ 7

Метод entries() объекта headers ответа сервера возвращает итератор

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

async function getHeaders ( url ) {
    let response = await fetch ( url )
    let iterator = response.headers.entries()
    do {
        var { done: stop, value: header } = iterator.next()
        header ? console.log ( `${header[0]}: ${header[1]}` ) : null
    } while ( !stop )
}

Вызовем функцию

getHeaders ( 'https://api.github.com/users/5' )

и увидим в консоли следующее:

cache-control: public, max-age=60, s-maxage=60
content-type: application/json; charset=utf-8
etag: W/"7870416c9818dd4ba65ab505535c7b79"
last-modified: Fri, 28 Dec 2018 06:04:01 GMT
x-github-media-type: github.v3; format=json
x-ratelimit-limit: 60
x-ratelimit-remaining: 58
x-ratelimit-reset: 1560744850

Примеры асинхронного генератора
8 9 10 11
☕ 11 Описание примера

🎓 Связные списки

☕ 12

Пусть у нас есть массив объектов

const objects = [
    { val: "first",  nextItem: "second" },
    { val: "forth",  nextItem: "fifth" },
    { val: "sixth",  nextItem: null },
    { val: "third",  nextItem: "forth" },
    { val: "fifth",  nextItem: "sixth" },
    { val: "second", nextItem: "third" }
]

Каждый элемент массива содержит свойство nextItem - ссылку на другой элемент этого же массива

Создадим протокол итерирования такого массива

Пусть элементы массива перебираются не в том порядке, в котором они расположены в массиве, а по новому протоколу, т.е. следующим будет выбираться элемент, указанный в свойстве nextItem текущего элемента

function* someGenerator ( objs ) {
    let currentItem = objs [ 0 ]
    let nextItem = objs [ 0 ]
    while ( !!nextItem ) {
        currentItem = nextItem
        nextItem = !!currentItem.nextItem ? 
            objs.find ( x => currentItem.nextItem === x.val )
            : null
        yield currentItem.val
    }
}

Генератор принимает в качестве аргумента ссылку на итерируемый массив

Создадим итератор для массива objects

var iterator = someGenerator ( objects )

Теперь можно использовать метод next() итератора iterator


📌 Изменим протокол итерирования массива

objects[Symbol.iterator] = function* () {
    let currentItem = this [ 0 ]
    let nextItem = this [ 0 ]
    while ( !!nextItem ) {
        currentItem = nextItem
        nextItem = !!currentItem.nextItem ? 
            this.find ( x => currentItem.nextItem === x.val )
            : null
        yield currentItem.val
    }
}

Теперь оператор for...of будет итерировать массив objects в нужном порядке

for ( let obj of objects )
    console.log ( obj )

Кроме того, при деструктуризации массива objects значения будут возвращены в указанном протоколом порядке

let [ a, b, c, d ] = objects

☕ 13
let btn = document.body.appendChild (
    document.createElement ( "button" )
)
btn.innerText = "new"
btn.onclick = function ( event ) {
    let ava = getAvatar.next()
    if ( !ava.done ) document.body.appendChild ( ava.value )
}

function* avaGenerator () {
    let num = 9
    while ( ++num < 99 ) {
        let ava = document.createElement ( "img" )
        ava.src = `https://www.shareicon.net/data/2015/12/14/2078${num}_face_300x300.png`
        ava.width = "80"
        yield ava
    }
}

let getAvatar = avaGenerator ()

🎓 yield*

☕ 14
const iterator1 = function* ( arg ) {
        while ( true ) {
            yield arg++
        }
}

const iterator2 = (
    function* ( arg ) {
        while ( true ) {
            arg < 5 ? yield arg++ : yield* iterator1(50)
        }
    }
)(0)


document.body.onclick = function ( event ) {
    let iter = iterator2.next()
    console.log ( iter.value )
}

☕ 15
const generator1 = function* ( arg ) {
    while ( true ) {
        arg++ < 5 ? yield "generator1: " + arg : yield* generator2()
    }
}

function* generator2() {
    while ( true ) {
        Math.random() > 0.3 ? yield "generator2" : yield* generator1(0)
    }
}
const iterator2 = generator2 (3)


document.body.onclick = function ( event ) {
    let iter = iterator2.next()
    console.log ( iter.value )
}
☕ 16
const generator = function* () {
    yield* [ 5, 4, 3, 2, 1 ]
    yield* "API"
    yield* arguments
}

const iterator = generator ( 10, 20, 30 )

document.body.onclick = function ( event ) {
    console.log ( iterator.next().value )
}

🎓 return()

☕ 17
const generator1 = function* () {
    while ( true ) {
        let x = Math.round ( Math.random() * 10 )
        x > 5 ? yield "generator1: " + x : yield* generator2()
    }
}

function* generator2() {
    while ( true ) {
        Math.random() > 0.3 ? yield "generator2" : yield* generator1()
    }
}
const iterator = generator2 (3)


document.body.onclick = function ( event ) {
    let iter = iterator.next()
    console.log ( iter.value )
    if ( iter.value === "generator1: 8" ) iterator.return()
}

Передача параметров

Поскольку генератор имеет внутренний "тормоз" - yield, и возобновление его работы с точки останова осуществляется внешним "пинком" - методом next() объекта-итератора, логично, что именно в этой точке появляется возможность передать какие-то данные функции-генератору

Для этого нужно, чтобы в точке останова генератора было присваивание:

...
let param = yield ...
...

Тогда аргумент, переданный при вызове next(), попадет в переменную param

☕ 18
iterator = ( function* ( arg ) {
    let ind = 0, 
        ret = arg, 
        d = new Date().getTime(),
        key;

    while ( true ) {
        key = d === new Date().getTime() ? `${d}[${ind++}]` : new Date().getTime();
        d = new Date().getTime();
        ret = yield { [ key ]: ret };
    }
})( "Hello" )

console.log ( iterator.next().value )
console.log ( iterator.next( "Welcome" ).value )
console.log ( iterator.next( "Who are you?" ).value )
console.log ( iterator.next( "Bye-bye..." ).value )

Symbol.asyncIterator

Если у объекта есть свойство [ Symbol.asyncIterator ], и это асинхронная функция, то этот объект можно итерировать циклом for await...of

⚠️ Однако в обычном смысле этот объект не является итерабельным, т.е. обычным оператором for...of его итерировать нельзя, и к нему нельзя применить оператор spread

☕ 19
const promise = val => new Promise (
    resolve => setTimeout (
      () => resolve ( val ),
      1000
    )
)

const browsers = {
    [ Symbol.asyncIterator ]: async function* () {
        yield promise ( "Chrome" )
        yield promise ( "Mozilla" )
        yield promise ( "Safari" )
        yield promise ( "IE" )
    },
    async show () {
        for await ( let browser of browsers )
            console.log ( browser )
    }
}

browsers.show()

Но мы можем добавить объекту оба свойства - и [ Symbol.asyncIterator ], и [ Symbol.iterator ]

☕ 20
const promise = val => new Promise (
    resolve => setTimeout (
      () => resolve ( val ),
      1000
    )
)

const browsers = {
    [ Symbol.asyncIterator ] : async function* () {
        yield promise ( "Chrome" )
        yield promise ( "FireFox" )
        yield promise ( "Safari" )
        yield promise ( "IE" )
    },
    
    [ Symbol.iterator ] : function* () {
        yield "Google",
        yield "Mozilla",
        yield "Mac",
        yield "Microsoft"
    },
    
    async show () {
        for await ( let browser of browsers )
            console.log ( browser )
    }
}

browsers.show()

console.log ( ...browsers )

Array.from ( browsers ).forEach ( browser => console.log ( browser ) )

© Irina H.Fylyppova 2018
Использование данных материалов или любой их части коммерческими школами ( курсами ) является нарушением авторских прав


Новая версия


1 2 3 4 5
6 7 8 9 10
11 12 13 14 15
16 17 18 19

Занятие 1

⤵️

Занятие 2

⤴️ ⤵️

Занятие 3

⤴️ ⤵️

Занятие 4

⤴️ ⤵️

Занятие 5

⤴️ ⤵️

Занятие 6

⤴️ ⤵️

Занятие 7

⤴️ ⤵️

Занятие 8

⤴️ ⤵️

Занятие 9

⤴️ ⤵️

Занятие 10

⤴️ ⤵️

Занятие 11

⤴️ ⤵️

Занятие 12

⤴️ ⤵️

Занятие 13

⤴️ ⤵️

Занятие 14

⤴️ ⤵️

Занятие 15

⤴️ ⤵️

Занятие 16

⤴️ ⤵️

Занятие 17

⤴️ ⤵️

Занятие 18

⤴️ ⤵️

Занятие 19

⤴️ ⤵️

⤴️

ico20 Дополнительно
dir-20 Справочная инфо

Clone this wiki locally