Generatorlar, ilg'or iteratsiya

Generatorlar

Oddiy funksiya bir marta ishga tushib, bir marta qiymat qaytaradi va tugaydi. Generator esa boshqacha โ€” u bir necha marta "to'xtab", bir necha bor qiymat qaytara oladi va keyin qoldirilgan joyidan davom etadi. Generatorlar iteratorlar bilan birga ishlaganda ma'lumotlar oqimini juda oson yaratishga imkon beradi.

Generator funksiyasi

Generator yaratish uchun maxsus sintaksisdan foydalaniladi: function* (yulduzcha bilan). Bunday funksiyalar generator funksiyalari deb ataladi.

Yulduzchani function so'zidan keyin ham, funksiya nomidan oldin ham qo'yish mumkin โ€” function* gen() yoki function *gen(). Odatda birinchi ko'rinish afzal ko'riladi.

function* generateSequence() {
  yield 1;
  yield 2;
  return 3;
}

Generator funksiyasining o'zi chaqirilganda kod bajarilmaydi. Uning o'rniga u maxsus generator obyektini qaytaradi. Bu obyekt bajarilishni boshqarishga xizmat qiladi.

generator-obyekt.js
// natija shu yerda chiqadi
Generator funksiyasini chaqirish uning tanasidagi kodni ishga tushirmaydi. U shunchaki kodni boshqaruvchi obyektni qaytaradi. Kod faqat next() chaqirilganda bajarila boshlaydi.

yield โ€” asosiy metod

Generatorning yuragi โ€” yield operatori. Aynan u generatorni oddiy funksiyadan ajratib turadi. yield ma'nosi: "bu yerda to'xta, qiymat qaytar va meni kutib tur".

Generator ishlashi uchun uning next() metodi chaqirilishi kerak. next() chaqirilganda kod eng yaqin yield <qiymat> gacha bajariladi (qiymat tashlab yuborilishi mumkin, u holda undefined bo'ladi). So'ng bajarilish to'xtaydi va qiymat tashqariga qaytariladi.

next() ning natijasi โ€” doim ikkita xususiyatli obyekt:

next-metod.js
// natija shu yerda chiqadi

Diqqat qiling: birinchi next() birinchi yield 1 gacha bajarildi va {value: 1, done: false} qaytardi. Ikkinchisi yield 2 gacha davom etib, {value: 2, done: false} qaytardi. Uchinchisi return 3 gacha yetib bordi โ€” bu generatorning tugashini bildiradi, shuning uchun done: true.

Generator tugagach (done: true), keyingi next() chaqiruvlari doim {value: undefined, done: true} qaytaradi. Tugagan generatorni qayta ishga tushirib bo'lmaydi โ€” buning uchun yangi generator obyekti yaratish kerak.
yield va return orasidagi farq: yield to'xtatadi lekin generator hali done emas; return esa generatorni butunlay tugatadi. Shuning uchun for..of return qiymatini e'tiborsiz qoldiradi (buni pastda ko'ramiz).

Generatorlar โ€” iterativlar (iterable)

Generator obyektlari iterativ (iterable) hisoblanadi. Bu degani, ularni for..of sikli bilan aylanib chiqish mumkin:

for-of.js
// natija shu yerda chiqadi

Bu next() ni qo'lda chaqirishdan ancha chiroyliroq ko'rinadi, shunday emasmi?

Muhim nozik jihat: yuqoridagi misol 1, 2, 3 ni ko'rsatadi, lekin 3 ni emas! Sababi โ€” for..of iteratsiyasi done: true bo'lgan oxirgi qiymatni e'tiborsiz qoldiradi. Shuning uchun agar barcha qiymatlar for..of orqali ko'rinishini xohlasangiz, ularni return emas, yield orqali qaytaring.

Quyidagi misolda farqni ko'ring โ€” return 3 ishlatilganda 3 ko'rinmaydi:

return-for-of.js
// natija shu yerda chiqadi

Spread sintaksisi bilan

Generatorlar iterativ bo'lgani uchun, spread sintaksisi ... ham ular bilan ishlaydi. Bu generator natijalarini massivga aylantirishning qulay usuli:

spread.js
// natija shu yerda chiqadi
Endilikda massiv, string, Map, Set ishlaydigan har qanday joyda generatorlarni ham ishlata olasiz โ€” chunki ularning barchasi umumiy iteratsiya protokoliga bo'ysunadi.

Iterativlar o'rniga generatorlardan foydalanish

Ilgari (masalan, iteratorlar mavzusida) biz range obyektiga Symbol.iterator qo'shib, uni iterativ qilgan bo'lishimiz mumkin. Generatorlar bu ishni ancha soddalashtiradi. Quyida โ€” generatorli, sodda va o'qishga oson variant:

range-generator.js
// natija shu yerda chiqadi

Bu ishlaydi, chunki range[Symbol.iterator]() endi generatorni qaytaradi, generator obyektida esa next() metodi va [Symbol.iterator] allaqachon mavjud. Ya'ni generator obyektining o'zi ham iterator, ham iterativ.

*[Symbol.iterator]() { ... } โ€” bu obyekt metodini generator sifatida e'lon qilishning qisqa yozuvi. Yulduzcha (*) metodni generatorga aylantiradi.

Cheksiz generatorlar

Generatorlar dangasa (lazy) โ€” ular qiymatni faqat so'ralganda ishlab chiqaradi. Shu sabab, cheksiz ketma-ketliklarni ham yaratish mumkin. Muhimi โ€” bunday generatorni cheksiz aylantirmang, aks holda dastur qotib qoladi.

cheksiz.js
// natija shu yerda chiqadi
Cheksiz generatorni for..of yoki [...gen] bilan to'liq aylanib chiqmang โ€” bu abadiy sikl va dastur to'xtab qolishiga olib keladi. Ulardan doim next() orqali, kerakli miqdorda qiymat oling.

Generator kompozitsiyasi: yield*

Generator kompozitsiyasi โ€” bir generatorni boshqasining ichiga "joylash" (embed qilish) imkoniyati. Buning uchun maxsus yield* sintaksisidan foydalaniladi.

Misol uchun, ketma-ket sonlar generatorimiz bo'lsin, keyin uni raqamlar va harflar ketma-ketligini yaratish uchun qayta ishlataylik:

yield-delegatsiya.js
// natija shu yerda chiqadi

yield* gen ifodasi gen generatorga delegatsiya qiladi: uning barcha yield lari xuddi tashqi generatorning o'zidan chiqqandek tashqariga uzatiladi. Natija xuddi ichki generatorning kodi tashqi generator ichiga to'g'ridan-to'g'ri yozilgandek bo'ladi.

Quyidagi ikki variant bir xil natija beradi:

yield-star-taqqoslash.js
// natija shu yerda chiqadi
yield* istalgan iterativ bilan ishlaydi โ€” nafaqat generatorlar, balki massivlar va stringlar bilan ham. Masalan, yield* [1, 2, 3] yoki yield* 'abc' ham to'g'ri ishlaydi.
yield-star-iterable.js
// natija shu yerda chiqadi

next(qiymat) orqali ma'lumot uzatish

Hozirgacha generatorlar ma'lumot manbaiga o'xshardi โ€” ular qiymat chiqarardi. Ammo generatorlar ikki tomonlama aloqa quroli: ularga ichkariga ham qiymat uzatish mumkin. Buning uchun generator.next(qiymat) ga argument beriladi.

Bu qiymat yield ifodasining natijasi bo'lib qaytadi. Ya'ni let result = yield ... yozganda, result ga aynan keyingi next(qiymat) dagi qiymat tushadi.

next-argument.js
// natija shu yerda chiqadi

Bu jarayonni bosqichma-bosqich tushunish muhim:

  1. Birinchi next() doim argumentsiz chaqiriladi (unga uzatilgan qiymat e'tiborsiz qoldiriladi), chunki hali kutayotgan yield yo'q. U kodni birinchi yield gacha yuguradi.
  2. Keyin next(4) chaqiriladi โ€” 4 qiymati generatorga qaytadi va yield ifodasining natijasi bo'lib, javob o'zgaruvchisiga tushadi.
  3. Generator keyingi yield gacha (yoki oxirigacha) davom etadi.

Ko'p savolli, murakkabroq misol:

next-korsatuv.js
// natija shu yerda chiqadi
next(qiymat) ni tushunishning eng oson yo'li: yield โ€” bu "ikki tomonli darvoza". Undan chiqadigan qiymat yield dan keyin yoziladi, unga kiradigan qiymat esa keyingi next() dan keladi.

generator.return va generator.throw

Generatorlarda next() dan tashqari yana ikki metod bor:

return-metod.js
// natija shu yerda chiqadi
return() chaqirilgach generator tugaydi โ€” keyingi barcha next() lar {value: undefined, done: true} qaytaradi.

Amaliy qo'llanishi

Generatorlar aynan qaysi vaziyatlarda foydali?

Quyida โ€” noyob ID generator, amaliy va foydali namuna:

id-generator.js
// natija shu yerda chiqadi

Yana bir misol โ€” daraxtsimon tuzilmani yield* rekursiya bilan tekislash:

daraxt-walk.js
// natija shu yerda chiqadi

Xulosa