IlmHamroh
JavaScript Full-stack/10-qism. JavaScript chuqur53/65-dars21 daqiqa
Mundarija (37)

JavaScript generatorlar chuqur: yield*, next(qiymat), return() va throw()

Qisqacha: yield* ishni boshqa generatorga yoki istalgan iterable'ga topshiradi: uning hamma qiymatlarini birma-bir tashqariga beradi. Aloqani esa next(qiymat) ikki tomonlama qiladi: berilgan qiymat to'xtab turgan yield ifodasining natijasiga aylanadi. Metodlardan return(x) generatorni tashqaridan yakunlaydi, throw(xato) esa to'xtagan yield joyida xato "otadi". Shu to'rt vosita generatorni korutinga — chaqiruvchi bilan navbatma-navbat gaplashadigan funksiyaga aylantiradi.

Bu darsda

  • yield* bilan bir nechta generatorni bittaga birlashtirasiz va ichma-ich tuzilmani rekursiv aylanasiz.
  • next(qiymat) bilan generatorga ma'lumot yuborasiz va birinchi next() argumenti nega yo'qolishini tushuntira olasiz.
  • return() va throw() generatorga qanday ta'sir qilishini bilasiz.
  • Korutin g'oyasini va redux-saga kabi kutubxonalar generatorni qanday ishlatishini tushunasiz.
  • vazifalar dagi qo'lda yozilgan iteratorni bitta yield* qatoriga qisqartirasiz.

Oldin bilishingiz kerak: Generator funksiyalar, O'z iterable obyektingizni yasash, Rekursiya asoslari, Stack trace va xato turlari.

1. Nega bu kerak?

O'tgan darsda generator bir tomonga gapirdi: qiymat berdi, to'xtadi, yana berdi. Chaqiruvchi faqat "keyingisi" deb so'radi. Real ishda esa ikki savol tez chiqadi.

Birinchi savol: bo'laklardan yig'ish. «Bahor» menyusi bo'limlardan iborat: issiq taomlar, ichimliklar. Har bo'lim — alohida generator. Butun menyu — ularning ketma-ket birlashmasi. Buni qanday qisqa yozamiz?

Ikkinchi savol: javob qaytarish. Kassa generatori buyurtmalar summasini yuritsin deylik: biz unga har taomning narxini beramiz, u esa yangi jamini qaytaradi. Hozircha generator faqat beradi — undan olish yo'lini bilmaymiz.

Hayotdan o'xshatish: ofitsiant va oshpaz. Ofitsiant oshxona eshigidan "bitta osh!" deydi va kutadi. Oshpaz oshni tayyorlab, likopchani eshikka qo'yadi va keyingi buyurtmani kutadi. Ikki tomon navbatma-navbat ishlaydi, har biri bir-biriga nimadir uzatadi. Generator va chaqiruvchi o'rtasida ham xuddi shunday "eshik" bor — yield. Bugun u eshikdan ikki tomonga ham narsa o'tishini ko'ramiz.

2. yield* — ishni topshirish

2.1 Muammo: bo'limlarni qo'lda ulash

js
function* issiqTaomlar() {
  yield "osh";
  yield "lag'mon";
}

function* ichimliklar() {
  yield "ko'k choy";
}

function* menyu() {
  for (const taom of issiqTaomlar()) {
    yield taom;
  }
  for (const taom of ichimliklar()) {
    yield taom;
  }
}

console.log([...menyu()]); // [ 'osh', "lag'mon", "ko'k choy" ]

Ishlaydi, lekin har bo'lim uchun uch qatorli bir xil sikl: "ichkaridagidan ol — tashqariga ber". Bo'limlar o'nta bo'lsa — o'ttiz qator takror.

2.2 Yechim: yield*

js
function* issiqTaomlar() {
  yield "osh";
  yield "lag'mon";
}

function* ichimliklar() {
  yield "ko'k choy";
}

function* menyu() {
  yield* issiqTaomlar();
  yield* ichimliklar();
}

console.log([...menyu()]); // [ 'osh', "lag'mon", "ko'k choy" ]

yield* (yulduzchali yield) — "bu iterable'ning hamma qiymatini birma-bir men nomimdan ber" degani. Inglizcha bu delegatsiya (delegation) — ishni boshqaga topshirish. menyu generatori yield* issiqTaomlar() ga yetganda, issiqTaomlar tugaguncha tashqaridagi har next() to'g'ridan-to'g'ri unga boradi. U tugagach — menyu keyingi qatordan davom etadi.

O'xshatish: brigadir. Buyurtmachi faqat brigadir bilan gaplashadi. Brigadir esa "devorni Ali quradi" deb ishni topshiradi. Ali tugatguncha buyurtmachi har g'ishtni Alidan oladi, lekin brigadir orqali. Ali tugadi — brigadir keyingi ishchiga o'tadi.

2.3 yield* istalgan iterable bilan ishlaydi

yield* faqat generatorni emas, har qanday iterable'ni qabul qiladi — massiv, satr, Set, Map, o'z iterable'ingiz:

js
function* namuna() {
  yield ["osh", "manti"];
  yield* ["osh", "manti"];
  yield* "ok";
}

console.log([...namuna()]);

Konsolda:

text
[ [ 'osh', 'manti' ], 'osh', 'manti', 'o', 'k' ]

Farqga qarang:

Yozuv Nima beradi
yield massiv massivning o'zini, bitta qiymat sifatida
yield* massiv massivning elementlarini, bittadan
yield* "ok" harflarni bittadan — satr ham iterable

yield* ichida aslida o'tgan darslardagi protokol ishlaydi: avval [Symbol.iterator](), keyin next() lar done: true gacha.

2.4 yield* ning natijasi

yield* ham ifoda: u ichki generatorning return qiymatiga teng. O'tgan darsda "return qiymatini for...of tashlab yuboradi" degan edik. yield* esa uni ushlaydi:

js
function* issiqTaomlar() {
  yield "osh";
  yield "lag'mon";
  return 2;
}

function* ichimliklar() {
  yield "ko'k choy";
  return 1;
}

function* menyu() {
  const a = yield* issiqTaomlar();
  const b = yield* ichimliklar();
  console.log(`Jami taomlar: ${a + b}`);
}

console.log([...menyu()]);

Konsolda:

text
Jami taomlar: 3
[ 'osh', "lag'mon", "ko'k choy" ]

Ichki generatorlar qiymatlarni yield bilan tashqariga berdi, return bilan esa faqat o'z brigadiriga hisobot berdi. Tashqi iste'molchi (spread) bu sonlarni ko'rmadi — ular ichkarida qoldi. Bu ikki xil kanal: yield — mijozga, return — boshliqqa.

2.5 Rekursiya: ichma-ich bo'limlar

yield* ning eng kuchli joyi — rekursiya bilan. Menyu bo'limlari ichida yana bo'limlar bo'lsin:

flowchart TD
  M["Menyu"] --> I["Issiq<br/>Osh, Manti"]
  M --> C["Ichimlik<br/>Ko'k choy"]
  C --> S["Sovuq<br/>Kompot"]

Hamma taomni qanday chiqaramiz? Har bo'lim uchun bir xil ish: o'z taomlarini ber, keyin har ichki bo'lim uchun xuddi shu ishni qil.

js
const menyuDaraxti = {
  nom: "Menyu",
  taomlar: [],
  bolimlar: [
    { nom: "Issiq", taomlar: ["Osh", "Manti"], bolimlar: [] },
    {
      nom: "Ichimlik",
      taomlar: ["Ko'k choy"],
      bolimlar: [
        { nom: "Sovuq", taomlar: ["Kompot"], bolimlar: [] },
      ],
    },
  ],
};

function* hammaTaomlar(bolim) {
  yield* bolim.taomlar;
  for (const ichki of bolim.bolimlar) {
    yield* hammaTaomlar(ichki);
  }
}

console.log([...hammaTaomlar(menyuDaraxti)]);

Konsolda:

text
[ 'Osh', 'Manti', "Ko'k choy", 'Kompot' ]

Ikki qatorli tana: yield* bolim.taomlar — o'z taomlari, yield* hammaTaomlar(ichki) — ichki bo'limga topshirish. Rekursiya asoslari darsidagi to'xtash sharti shu yerda ham bor: bolimlar bo'sh bo'lsa, for aylanmaydi va chuqurga tushish to'xtaydi.

Natija ham dangasa: birinchi ikki taomni olish uchun "Sovuq" bo'limiga tushilmaydi ham. Massiv bilan yozganda esa butun daraxtni avval yig'ish kerak bo'lardi.

Tekshirib ko'ring: hammaTaomlar dagi yield* hammaTaomlar(ichki) ni yield hammaTaomlar(ichki) (yulduzchasiz) qilsak, [...hammaTaomlar(menyuDaraxti)] nima beradi?

Javob

[ 'Osh'… ] emas, balki ichki bo'limlar o'rnida generator obyektlari: [ Object [Generator] {}, Object [Generator] {} ] (menyuning o'z taomlari bo'sh). yield ichki generatorni bitta qiymat sifatida berdi — uni ochmadi. Ichma-ich tuzilmani "tekislash" uchun aynan yield* kerak.

3. next(qiymat) — generatorga ma'lumot yuborish

3.1 yield — ikki tomonlama eshik

Hozirgacha yield ni ko'rsatma (statement) sifatida yozdik: yield "osh";. Aslida yield — ifoda, uning natijasi bor. Natija — keyingi next() ga berilgan argument:

js
const narx = yield jami;

Bu qator ikki ishni ikki xil vaqtda qiladi:

  1. yield jami — jami ni tashqariga beradi va to'xtaydi.
  2. Keyinroq kimdir next(35000) ni chaqiradi — generator uyg'onadi, yield jami ifodasi 35000 ga aylanadi va narx ga yoziladi.

Eshikdan bir tomonga likopcha chiqdi, keyin narigi tomondan buyurtma qog'ozi kirdi.

3.2 Kassa hisobchisi

js
function* hisobchi() {
  let jami = 0;
  while (true) {
    const narx = yield jami;
    jami += narx;
  }
}

const kassa = hisobchi();
console.log(kassa.next().value); // 0
console.log(kassa.next(35000).value); // 35000
console.log(kassa.next(5000).value); // 40000

Kassa — cheksiz generator (Generator funksiyalar). U har narxni qabul qilib, yangi jamini qaytaradi. jami uning ichida, tashqaridan uni buzib bo'lmaydi. Bu closure'dagi private holatga o'xshaydi, lekin endi u bilan "suhbat" qurish mumkin.

O'zingiz to'ldiring. Yangi hisobchi() generatoriga ketma-ket next(), next(28000), next(30000) chaqirildi. Uchinchi chaqiruvning value si — [:58000]. Undan keyin next(5000) chaqirilsa — [:63000].

3.3 Qadamma-qadam

Keyingi ni bosing. next() ning argumenti qaysi qadamda generator ichiga kirishini kuzating:

3.4 Birinchi next() argumenti yo'qoladi

Vizualning ikkinchi qadamiga qayting: birinchi next() paytida hech qanday yield kutib turmagan. Tana endi boshlanyapti. Shuning uchun birinchi next() ga berilgan argument hech qayerga tushmaydi:

js
function* savolJavob() {
  const javob = yield "Qaysi taom?";
  console.log(`Javob: ${javob}`);
}

const suhbat = savolJavob();
console.log(suhbat.next("osh").value); // Qaysi taom?
suhbat.next("manti");

Konsolda:

text
Qaysi taom?
Javob: manti

"osh" yo'qoldi — xato ham chiqmadi. Birinchi next() — "ishga tushirish": u generatorni birinchi yield gacha olib boradi. Ma'lumot ikkinchi next() dan boshlab kiradi. Shuning uchun ikki tomonlama generatorlarda birinchi next() odatda argumentsiz chaqiriladi, javobi esa ("Qaysi taom?" kabi) "savol" vazifasini bajaradi.

3.5 Korutin g'oyasi

Kassa misolida ikki kod bo'lagi navbatma-navbat ishladi: chaqiruvchi narx berdi — generator hisobladi va to'xtadi — chaqiruvchi yana narx berdi. Hech biri ikkinchisini to'liq "chaqirib tugatmadi"; ular bir-biriga boshqaruvni uzatib turdi.

sequenceDiagram
  participant C as Chaqiruvchi
  participant G as hisobchi
  C->>G: next()
  G-->>C: 0 (yield)
  C->>G: next(35000)
  Note over G: jami += 35000
  G-->>C: 35000 (yield)
  C->>G: next(5000)
  G-->>C: 40000 (yield)

Bunday funksiya korutin (coroutine) deb ataladi — "hamkor funksiya": u o'z ishini yarim yo'lda to'xtatib, boshqaruvni boshqa kodga beradi va keyin to'xtagan joyidan davom etadi. JavaScript generatori — korutinning soddalashgan turi: u boshqaruvni faqat o'zini chaqirgan tomonga qaytara oladi. Bu cheklov amalda ancha qulay: kim kimni uyg'otishi doim aniq.

Tekshirib ko'ring: const kassa = hisobchi(); dan keyin birinchi chaqiruv kassa.next(35000) bo'lsa, value nima bo'ladi? Keyingi kassa.next(5000) chi?

Javob

Birinchisi — 0: argument yo'qoldi, generator faqat birinchi yield jami gacha bordi. Ikkinchisi — 5000: endi narx = 5000, jami = 0 + 5000. 35 000 so'm hisobga kirmadi. Shuning uchun ikki tomonlama generatorni avval argumentsiz next() bilan ishga tushiring.

4. return(qiymat) — tashqaridan yakunlash

Generatorni oxirigacha aylanmasdan to'xtatish kerak bo'lsa — return() metodi:

js
function* taomlar() {
  yield "osh";
  yield "manti";
  yield "somsa";
}

const g = taomlar();
console.log(g.next()); // { value: 'osh', done: false }
console.log(g.return("bekor")); // { value: 'bekor', done: true }
console.log(g.next()); // { value: undefined, done: true }

return("bekor") generatorni to'xtagan yield joyida, xuddi u yerda return "bekor"; yozilgandek, yakunladi. Generator "Tugagan" holatga o'tdi — keyingi next() lar faqat done: true beradi. "manti" va "somsa" hech qachon berilmadi.

O'z iterable obyektingizni yasash darsidagi return() ni eslang — for...of dagi break uni avtomatik chaqiradi. Generatorda bu metod tayyor. yield* orqali topshirilgan bo'lsa, return() ichki generatorga ham yetib boradi: brigadir ishni to'xtatsa, Ali ham to'xtaydi.

Ichki generator yopilayotganda tozalash ishini bajarishi mumkin — buning uchun finally bloki ishlatiladi. Uni try / catch / finally darsida o'rganamiz, hozir bilish shart emas.

Tekshirib ko'ring: menyu() generatori yield* issiqTaomlar() ichida, "osh" dan keyin to'xtab turibdi. Tashqaridan menyu ga return() chaqirilsa, "lag'mon" beriladimi? menyu dagi keyingi yield* ichimliklar() chi?

Javob

Ikkalasi ham yo'q. return() avval ichki issiqTaomlar ga yetib boradi va uni yopadi, keyin menyu ning o'zini ham to'xtagan joyida yakunlaydi. yield* dan keyingi qatorlar bajarilmaydi. Brigadir ishni to'xtatdi — na Ali, na keyingi ishchi davom etmaydi.

5. throw(xato) — generator ichiga xato otish

5.1 Xato to'xtagan yield da paydo bo'ladi

Uchinchi metod — throw(). U generatorni uyg'otadi, lekin qiymat bilan emas, xato bilan: to'xtagan yield ifodasi o'rnida xato chiqadi.

Xato uchun Error obyekti kerak — xato xabarini saqlaydigan maxsus obyekt: new Error("matn"). Uni throw va Error obyekti darsida batafsil o'rganamiz; hozircha "xato xabari solingan quti" deb biling.

js
function* buyurtmalar() {
  yield "osh";
  yield "manti";
}

const g = buyurtmalar();
g.next();
g.throw(new Error("Buyurtma bekor qilindi"));

Konsolda:

text
Error: Buyurtma bekor qilindi

Generator xatoni ichkarida ushlamadi. Natijada xato g.throw(...) chaqirilgan joyga — tashqariga — qaytib chiqdi va dastur to'xtadi. Node uning ostida stack trace ham chiqaradi (Stack trace va xato turlari). Generator esa endi tugagan: g.next() — { value: undefined, done: true }.

5.2 Generator xatoni ushlasa

Generator ichida xatoni ushlash mumkin. Buning uchun try / catch konstruksiyasi bor: try ichida xato chiqsa, dastur yiqilmaydi — catch bloki ishlaydi. Uni try / catch / finally darsida to'liq o'rganamiz — bu yerda faqat generatorga ta'sirini ko'ramiz. catch (xato) dagi xato — ushlangan Error obyekti, xato.message — uning ichidagi xabar matni:

js
function* oshxona() {
  while (true) {
    try {
      yield "buyurtma kutilmoqda";
    } catch (xato) {
      console.log(`Oshxona: ${xato.message}, qayta yoqildi`);
    }
  }
}

const g = oshxona();
console.log(g.next().value);
console.log(g.throw(new Error("Gaz o'chdi")).value);
console.log(g.next().value);

Konsolda:

text
buyurtma kutilmoqda
Oshxona: Gaz o'chdi, qayta yoqildi
buyurtma kutilmoqda
buyurtma kutilmoqda

Xato yield joyida chiqdi, catch uni ushladi va xabarini chiqardi. Sikl davom etdi va keyingi yield ga yetdi — throw() shu qiymatni qaytardi (uchinchi qator). Generator tirik: oxirgi next() yana ishladi (to'rtinchi qator).

5.3 Boshlanmagan generatorga throw()

Generator hali bitta ham next() olmagan bo'lsa, uning ichida try ga kirilmagan — ushlaydigan joy yo'q. throw() xatoni darhol tashqariga qaytaradi va generator tanasiga umuman kirmasdan "Tugagan" holatga o'tadi. Ikki tomonlama generatorlarda birinchi next() ni unutmaslikning yana bir sababi.

Tekshirib ko'ring: next(), return(x) va throw(xato) — uchalasi ham generatorni to'xtagan yield dan uyg'otadi. Ular nimasi bilan farq qiladi?

Javob

Farq — to'xtagan yield ifodasi nimaga aylanishida. Agar next(x) bo'lsa — x qiymatiga, va kod davom etadi. Agar return(x) bo'lsa — xuddi o'sha joyda return x; yozilgandek: generator yakunlanadi. Agar throw(xato) bo'lsa — o'sha joyda xato chiqqandek. Ushlansa, kod davom etadi; ushlanmasa, xato tashqariga chiqadi va generator tugaydi.

6. Amaliyotda: generator va "ijrochi"

6.1 Generator nima qilishni aytadi, ijrochi bajaradi

Endi to'rt vositani bitta naqshga yig'amiz. Generator o'zi hech qanday ish qilmasin — faqat topshiriq obyektlarini yield qilsin: "osh narxini top", "chek chiqar". Alohida ijrochi funksiya topshiriqni bajarib, javobni next(javob) bilan qaytarsin:

js
const narxlar = { osh: 35000, manti: 30000, "ko'k choy": 5000 };

function* buyurtmaJarayoni() {
  const osh = yield { tur: "narx", taom: "osh" };
  const choy = yield { tur: "narx", taom: "ko'k choy" };
  yield { tur: "chek", matn: `Jami: ${osh + choy} so'm` };
}

function ijroEt(generator) {
  let qadam = generator.next();
  while (!qadam.done) {
    const topshiriq = qadam.value;
    let javob;
    if (topshiriq.tur === "narx") {
      javob = narxlar[topshiriq.taom];
    } else if (topshiriq.tur === "chek") {
      console.log(topshiriq.matn);
    }
    qadam = generator.next(javob);
  }
}

ijroEt(buyurtmaJarayoni());

Konsolda:

text
Jami: 40000 so'm

buyurtmaJarayoni oddiy ketma-ket kod kabi o'qiladi: "osh narxi kerak, choy narxi kerak, chek chiqar". Lekin u narxlarni o'zi qayerdan olishni bilmaydi va konsolga o'zi yozmaydi. Bu ishlarni ijrochi qiladi. Ijrochi — Iteratsiya protokoli darsidagi qo'lda yozilgan for...of ning o'zi, faqat next() ga javob uzatadi.

6.2 Nega bu foydali: sinash oson

Generator "nima qilish kerak"ni aytadi, xolos. Shuning uchun uni haqiqiy narxlarsiz, soxta javoblar bilan sinash mumkin:

js
function* buyurtmaJarayoni() {
  const osh = yield { tur: "narx", taom: "osh" };
  const choy = yield { tur: "narx", taom: "ko'k choy" };
  yield { tur: "chek", matn: `Jami: ${osh + choy} so'm` };
}

const sinov = buyurtmaJarayoni();
console.log(sinov.next().value);
console.log(sinov.next(100).value);
console.log(sinov.next(1).value);

Konsolda:

text
{ tur: 'narx', taom: 'osh' }
{ tur: 'narx', taom: "ko'k choy" }
{ tur: 'chek', matn: "Jami: 101 so'm" }

Biz ijrochi rolini qo'lda o'ynadik: "osh — 100, choy — 1" dedik va generator to'g'ri hisoblaganini ko'rdik. Haqiqiy narx bazasi, server yoki printer kerak bo'lmadi. Har qadamda generator nimani so'raganini ham tekshirdik.

6.3 Redux-saga va async/await ning ildizi

Aynan shu naqsh mashhur kutubxonalarning ichida turibdi:

  • Redux-saga — React ilovalarida server so'rovlari kabi "yon ishlar"ni boshqaradigan kutubxona (Redux — ilova holatini saqlaydigan kutubxona, uni 18-qismda o'rganamiz; hozir bilish shart emas). Unda har jarayon — generator. Quyidagi parchadagi hamma nomni tushunish shart emas — faqat shaklga qarang: yield o'ng tomonida topshiriq, chap tomonida (const javob =) ijrochidan kelgan javob, xuddi bizning buyurtmaJarayoni dagidek:
js
function* buyurtmaSaga(action) {
  const javob = yield call(api.buyurtmaBer, action.taom);
  yield put({ type: "BUYURTMA_QABUL", javob });
}

call(...) va put(...) hech narsani bajarmaydi — ular bizdagi { tur: "narx", ... } kabi topshiriq obyektlarini yasaydi. Saga ijrochisi so'rovni yuboradi, javob kelganda next(javob) bilan generatorni uyg'otadi. Sinovda esa haqiqiy server o'rniga soxta javob beriladi — xuddi bizning sinov kabi.

  • async/await — 11-qismdagi asinxron kodning asosiy sintaksisi. Undan oldin, 2014–2016 yillarda, dasturchilar xuddi shu naqshni ishlatgan (co kutubxonasi). Generator kutish kerak bo'lgan narsani yield qiladi. Ijrochi esa natija kelganda generatorni next(natija) bilan davom ettiradi. async/await shu g'oyani tilning o'ziga olib kirdi. Uni async / await darsida o'rganasiz — o'shanda bugungi ijroEt ni eslang.

7. Ko'p uchraydigan xatolar

7.1 Birinchi next() ga ma'lumot berish

«Birinchi next() argumenti yo'qoladi» bo'limidagi holat. Xato xabari yo'q — ma'lumot shunchaki yo'qoladi, hisob noto'g'ri chiqadi. Tuzatish: ikki tomonlama generatorni avval generator.next() bilan ishga tushiring, ma'lumotni ikkinchisidan boshlab bering.

7.2 yield va yield* ni chalkashtirish

yield ichkiGenerator() — generator obyektining o'zini beradi, yield massiv — massivning o'zini. Natijada Object [Generator] {} yoki ichma-ich massiv ko'rasiz. Tuzatish: ichidagilarni bittadan berish kerak bo'lsa — yield*.

7.3 yield* ga iterable bo'lmagan qiymat

js
function* soni() {
  yield* 5;
}
console.log([...soni()]);

Konsolda:

text
TypeError: number 5 is not iterable (cannot read property Symbol(Symbol.iterator))

Tarjimasi: "5 soni iterable emas (Symbol.iterator xususiyatini o'qib bo'lmadi)". yield* ham protokol bilan ishlaydi — u qiymatdan [Symbol.iterator]() ni so'raydi (Iteratsiya protokolidagi new Map(obyekt) xatosi bilan bir oila). Ko'pincha sabab — yield* ga massiv o'rniga undefined kelishi (masalan, obyektda taomlar xususiyati yo'q). Tuzatish: bitta qiymat bo'lsa — yield; ro'yxat bo'lsa — haqiqatan ham ro'yxat kelayotganini tekshiring.

7.4 Ushlanmagan throw()

Generator ichida try / catch bo'lmasa, throw() xatoni chaqiruvchiga qaytaradi va dastur to'xtaydi («Xato to'xtagan yield da paydo bo'ladi» bo'limi). Generator ham tugaydi — uni qayta ishlatib bo'lmaydi. Tuzatish: generator xatoga "chidashi" kerak bo'lsa — yield ni try ichiga oling; aks holda xatoni tashqarida ushlang (try / catch / finally).

7.5 Tugagan generatordan javob kutish

return() yoki ushlanmagan xatodan keyin generator "Tugagan". Unga next(qiymat) bilan yangi ma'lumot berish hech narsa qilmaydi — har safar { value: undefined, done: true }. Tuzatish: yangi suhbat uchun generator funksiyani qayta chaqiring.

8. Mashqlar

1-mashq (oson): Kunlik menyu

nonushta() ("tuxum", "choy") va tushlik() ("osh", "salat") generatorlarini yozing. Keyin kunlikMenyu() generatori ikkalasini yield* bilan ulasin va oxirida "shirinlik" ni oddiy yield bilan qo'shsin.

Yechim
js
function* nonushta() {
  yield "tuxum";
  yield "choy";
}

function* tushlik() {
  yield "osh";
  yield "salat";
}

function* kunlikMenyu() {
  yield* nonushta();
  yield* tushlik();
  yield "shirinlik";
}

console.log([...kunlikMenyu()]);

Konsolda:

text
[ 'tuxum', 'choy', 'osh', 'salat', 'shirinlik' ]

yield* va yield bitta generatorda bemalol aralashadi: biri butun ro'yxatni topshiradi, ikkinchisi bitta qiymat qo'shadi.

2-mashq (o'rta): Ichma-ich massivni tekislash

tekisla(massiv) generatorini yozing: u istalgan chuqurlikdagi ichma-ich massivning hamma oddiy qiymatlarini bittadan bersin. Element massivmi — Array.isArray(x) bilan tekshiring. Rekursiya va yield* dan foydalaning.

js
const buyurtmalar = ["osh", ["manti", ["somsa", "choy"]], "salat"];
console.log([...tekisla(buyurtmalar)]);
// [ 'osh', 'manti', 'somsa', 'choy', 'salat' ]
Yechim
js
function* tekisla(massiv) {
  for (const element of massiv) {
    if (Array.isArray(element)) {
      yield* tekisla(element);
    } else {
      yield element;
    }
  }
}

const buyurtmalar = ["osh", ["manti", ["somsa", "choy"]], "salat"];
console.log([...tekisla(buyurtmalar)]);

Konsolda:

text
[ 'osh', 'manti', 'somsa', 'choy', 'salat' ]

Massiv bo'lsa — o'ziga topshiradi (rekursiya), oddiy qiymat bo'lsa — beradi. Bu flat(Infinity) ning dangasa nusxasi: flat darhol yangi massiv yasaydi, tekisla esa qiymatlarni so'ralganda bittadan beradi.

3-mashq (qiyin): Baholarning o'rtachasi

«Bahor» mehmonlari 1 dan 5 gacha baho qo'yadi. ortacha() generatorini yozing: u har next(baho) da shu paytgacha kelgan baholarning o'rtachasini qaytarsin. Birinchi, ishga tushiruvchi next() undefined qaytarsin.

js
const reyting = ortacha();
reyting.next();
console.log(reyting.next(5).value); // 5
console.log(reyting.next(4).value); // 4.5
console.log(reyting.next(3).value); // 4
Yechim
js
function* ortacha() {
  let jami = 0;
  let soni = 0;
  let natija;
  while (true) {
    const baho = yield natija;
    jami += baho;
    soni++;
    natija = jami / soni;
  }
}

const reyting = ortacha();
reyting.next();
console.log(reyting.next(5).value); // 5
console.log(reyting.next(4).value); // 4.5
console.log(reyting.next(3).value); // 4

natija boshida undefined — birinchi yield natija shuni beradi. Keyin har baho kelganda jami va soni yangilanadi va yangi o'rtacha keyingi yield da chiqadi. Holat (jami, soni) generator ichida yashirin — tashqi kod faqat baho beradi va javob oladi.

4-mashq: Vazifalar qadami — iterator bitta qatorga, branch'ni yakunlash

O'z iterable obyektingizni yasash darsida refactor/iteratsiya branch'ida VazifalarRoyxati ga 15 qatorli qo'lda yozilgan iterator qo'shgan edingiz. Endi uni generator va yield* bilan qayta yozing. Xulq o'zgarmasin. So'ng branch'ni main ga qo'shing.

Yechim

VazifalarRoyxati dagi butun [Symbol.iterator]() { ... } metodi o'rniga:

js
*[Symbol.iterator]() {
  yield* this.#vazifalar;
}

Bitta qator. * — metod generator; yield* — ishni ichki massivning o'z iteratoriga topshiradi. this to'g'ri ishlaydi — generator metod royxatda chaqiriladi. Qo'lda yozilgan variantdagi hamma narsa saqlandi: { value, done }, o'zini qaytaradigan [Symbol.iterator](), break da return(), hatto massivni "jonli" ko'rish — chunki yield* massivning o'z iteratoriga topshirdi.

Eski va yangi variant bir xil ishlashini Node'da tekshiring:

js
class QoldaRoyxat {
  #vazifalar = ["Non olish", "DOM darsini takrorlash"];
  qosh(matn) {
    this.#vazifalar.push(matn);
  }
  [Symbol.iterator]() {
    const vazifalar = this.#vazifalar;
    let i = 0;
    return {
      next() {
        if (i < vazifalar.length) {
          return { value: vazifalar[i++], done: false };
        }
        return { value: undefined, done: true };
      },
      [Symbol.iterator]() {
        return this;
      },
    };
  }
}

class GeneratorRoyxat {
  #vazifalar = ["Non olish", "DOM darsini takrorlash"];
  qosh(matn) {
    this.#vazifalar.push(matn);
  }
  *[Symbol.iterator]() {
    yield* this.#vazifalar;
  }
}

for (const Klass of [QoldaRoyxat, GeneratorRoyxat]) {
  const royxat = new Klass();
  const iterator = royxat[Symbol.iterator]();
  const birinchi = iterator.next().value;
  royxat.qosh("Choy olish");
  const qolgani = [...iterator];
  console.log(Klass.name, birinchi, qolgani.length);
}

Konsolda:

text
QoldaRoyxat Non olish 2
GeneratorRoyxat Non olish 2

Ikkalasi ham aylanish o'rtasida qo'shilgan "Choy olish" ni ko'rdi (qolgani — 2 ta). Xulq bir xil — farqi faqat kod hajmida.

Brauzerda butun ilovani sinab chiqing:

  • Uch vazifa, "2 ta vazifa qoldi, 1 ta bajarildi"; qo'shish, belgilash, «Faol» filtrida belgilash (fokus qo'shniga), tahrirlash (Enter, Esc), o'chirish, takror va bo'sh matn xatolari — avvalgidek.
  • Console'da: [...holat.vazifalar].length — vazifalar soni; holat.vazifalar[Symbol.iterator]() — Node formatida Object [Generator] {}. Chrome konsoli uni generator metod nomi bilan, [Symbol.iterator] {<suspended>} ko'rinishida chiqaradi — ma'nosi bir xil: to'xtab turgan generator.

Commit va branch'ni yakunlash:

bash
git add assets/js/vazifalar.js
git commit -m "Ro'yxat: iteratorni generator bilan yoz"
git switch main
git merge refactor/iteratsiya
git branch -d refactor/iteratsiya
git push

Bu ikki darsdagi o'zgarishlar:

Dars vazifalar.js da
O'z iterable'ingiz [Symbol.iterator]() — qo'lda, 15 qator
shu dars *[Symbol.iterator]() { yield* this.#vazifalar; }

Pages bir-ikki daqiqada yangilanadi. Foydalanuvchi farq sezmaydi — royxat esa endi tilning umumiy "aylanish tili"da gapiradi.

9. Real ishda

  • Redux-saga. Yirik React ilovalarida (bank, internet-do'kon, yetkazib berish xizmatlari) murakkab jarayonlar — to'lov, qayta urinish, bekor qilish — saga generatorlari bilan yoziladi. «Generator va "ijrochi"» bo'limidagi naqsh — uning yuragi.
  • Daraxt bo'ylab yurish. Fayl tizimi, DOM, menyu kategoriyalari, izohlar va ularga javoblar — hammasi daraxt. yield* bilan rekursiv aylanish — eng qisqa va dangasa yo'l. Algoritmlar qismida (14-qism) daraxt va graflarni ham shunday aylanasiz.
  • Kutubxona ichlari. Babel (yangi JavaScript'ni eski brauzerlar tushunadigan shaklga o'giruvchi vosita) va TypeScript (JavaScript'ga turlar qo'shadigan til) — ikkalasini kursda alohida o'rganamiz. Ular eski brauzerlar uchun async/await ni aynan generator + ijrochi ko'rinishiga o'girgan. Bugun ham eski loyihalarning yig'ilgan kodida __generator yordamchisini uchratasiz.
  • Intervyu. "yield* nima qiladi?", "next() ga argument berish nima beradi?", "async/await generatorlar bilan qanday bog'liq?" — o'rta va yuqori darajadagi intervyu savollari.

Xulosa

  • yield* iterable — ishni topshirish: ichki iterable'ning hamma qiymatini bittadan beradi; natijasi — ichki generatorning return qiymati.
  • yield* + rekursiya — ichma-ich tuzilmani (daraxtni) dangasa aylanishning eng qisqa yo'li.
  • const x = yield y — y tashqariga chiqadi, keyingi next(qiymat) dagi qiymat x ga tushadi. Birinchi next() argumenti yo'qoladi — u faqat ishga tushiradi.
  • return(x) — to'xtagan joyda yakunlaydi; throw(xato) — to'xtagan joyda xato chiqaradi (ichkarida ushlansa — davom etadi).
  • Generator + ijrochi — korutin naqshi: generator topshiriq beradi, ijrochi bajaradi. Redux-saga va async/await shu g'oyadan o'sib chiqqan.

Keyingi dars: Iterator helpers — iteratorlarda ham map, filter, take bor: ular massiv yasamay, dangasa ishlaydi.

Manbalar

  • MDN: "yield*", "Generator.prototype.next()", "Generator.prototype.return()", "Generator.prototype.throw()" — developer.mozilla.org
  • redux-saga hujjatlari: "Beginner Tutorial", "Declarative Effects" — redux-saga.js.org
  • ECMAScript spetsifikatsiyasi: "Generator Objects", "yield*" — tc39.es/ecma262
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
JavaScript generatorlar chuqur: yield, next(qiymat), return() va throw() — IlmHamroh