Series: JavaScript Basics Lesson 41

Generator

Generator nima, function*/yield sintaksisi, Generator = Iterator + Iterable, .next(value) orqali ikki tomonlama aloqa, yield*, va V8 ichida Generator holat mashinasi (JSGeneratorObject) sifatida qanday amalga oshirilishi.

41-dars

Generator

1. Kirish — protokollardan avtomatlashtirishga

Oldingi ikki mavzuda Iterable va Iterator Protocol'larni qo'lda yozishni ko'rdik — .next(), .return(), .throw(), holatni (state) qo'lda saqlash. Generator — aynan shu protokollarni til darajasida, avtomatik yaratib beruvchi mexanizm. Bu mavzu — oldingi ikkitasining tabiiy davomi va amaliy natijasi.


2. Generator nima — asosiy g'oya

Oddiy funksiya chaqirilganda to'liq ishlab tugaydi — to'xtab, keyin davom etib bo'lmaydi. Generator esa — pauza qila oladigan, keyin xuddi shu joydan davom eta oladigan funksiya.

function* simpleGen() {
  console.log('boshlandi');
  yield 1;
  console.log('davom etdi');
  yield 2;
  console.log('tugadi');
}

const gen = simpleGen();
console.log('gen yaratildi, lekin hali ishlamagan');

gen.next(); // 'boshlandi'    → { value: 1, done: false }
gen.next(); // 'davom etdi'   → { value: 2, done: false }
gen.next(); // 'tugadi'       → { value: undefined, done: true }

Bu yerda muhim narsa: simpleGen() chaqirilganda funksiya tanasi hali ishlamaydi! console.log('gen yaratildi...') 'boshlandi'dan oldin chiqadi. simpleGen() faqat Generator obyekt (Iterator + Iterable) yaratadi, lekin kod bajarilishi faqat birinchi .next() chaqirilganda boshlanadi.

Bu — Generator'ning eng fundamental xususiyati: lazy execution (dangasa bajarilish). Kod faqat kerak bo'lganda, qadamma-qadam ishga tushadi.


3. Syntax — function* va yield

function* gen() { }      // declaration
const gen2 = function* () { };  // expression
const obj = { *gen3() { } };    // method shorthand
class C { *gen4() { } }          // class method

Yulduzcha (*) qayerga qo'yilishi — stil masalasi (function* yoki function * ikkalasi ham ishlaydi), lekin ko'p codebase function* (funksiya nomiga yopishtirilgan) formatini tanlaydi.

yield — bu operator, faqat generator funksiya ichida ishlatilishi mumkin:

yield 5;        // qiymat "chiqar" va pauza qil
const x = yield; // pauza qil, tashqaridan qiymat "kirit"

4. Generator = Iterator + Iterable (ikkalasi birdaniga)

Bu — oldingi ikki mavzuni to'g'ridan-to'g'ri bog'laydigan qism. Generator obyekt avtomatik ravishda ikkala protokolga ham to'liq mos:

function* gen() { yield 1; yield 2; yield 3; }

const g = gen();

typeof g.next;              // 'function' — Iterator
typeof g[Symbol.iterator];  // 'function' — Iterable

g[Symbol.iterator]() === g; // true — self-iterable!

Shuning uchun Generator to'g'ridan-to'g'ri for...of, spread, destructuring bilan ishlaydi — hech qanday qo'shimcha kod yozmasdan:

[...gen()];              // [1, 2, 3]
for (const x of gen()) { console.log(x); } // 1 2 3
const [a, b] = gen();    // a=1, b=2

Qiyoslang: oldingi mavzuda Range classini qo'lda yozgan edik — [Symbol.iterator]() ichida qo'lda current, end state saqlab, .next() ichida shart-sharoit yozgan edik. Generator bilan xuddi shu narsani ancha qisqa yozamiz:

// Qo'lda (oldingi mavzu):
class Range {
  constructor(start, end) { this.start = start; this.end = end; }
  [Symbol.iterator]() {
    let cur = this.start, end = this.end;
    return { next: () => cur <= end
      ? { value: cur++, done: false }
      : { value: undefined, done: true } };
  }
}

// Generator bilan — bir xil natija, ancha oddiy:
function* range(start, end) {
  for (let i = start; i <= end; i++) {
    yield i;
  }
}

[...range(1, 3)]; // [1, 2, 3]

Generator — Iterator/Iterable protokolini qo'lda yozish uchun sintaktik qulaylik (syntactic sugar). Ichida V8 baribir .next(), { value, done } bilan ishlaydi — faqat siz buni qo'lda yozmaysiz, engine avtomatik quradi.


5. .next(value) — Generator'ga qiymat yuborish (ikki tomonlama aloqa)

Bu — Generator'ning eng kam tushuniladigan, lekin eng kuchli qismi. yield — bu ifoda (expression), ya'ni u qiymatga teng bo'lishi mumkin:

function* conversation() {
  const name = yield 'Ismingiz nima?';
  const age = yield `Salom ${name}, yoshingiz nechida?`;
  return `${name}, ${age} yoshda ekansiz`;
}

const c = conversation();

c.next();          // { value: 'Ismingiz nima?', done: false } — birinchi yield gacha ishlaydi
c.next('Ali');      // 'Ali' → `name` ga yoziladi; { value: 'Salom Ali, ...?', done: false }
c.next('25');       // '25' → `age` ga yoziladi; { value: 'Ali, 25 yoshda ekansiz', done: true }

Bu yerda eng ko'p chalkashadigan nuqta: birinchi .next()ga berilgan argument hech qachon ishlatilmaydi — chunki birinchi .next() faqat kodni birinchi yieldgacha ishga tushiradi, hali hech qanday yield natija kutmayapti (kod hali boshlanmagan edi). Faqat ikkinchi .next('Ali') chaqirilganda, 'Ali' qiymati birinchi yieldning natijasi sifatida kod ichiga kiradi.

Buni vizual qadamma-qadam ko'ramiz:

.next()        → kod ishga tushadi → 'Ismingiz nima?' gacha yetadi → PAUZA (yield 1-nuqta)
.next('Ali')   → 'Ali' birinchi yield'ning QIYMATI bo'ladi → name='Ali' → ikkinchi yield gacha → PAUZA
.next('25')    → '25' ikkinchi yield'ning QIYMATI bo'ladi → age='25' → return → TUGADI

Bu — nima uchun Generator ba'zan coroutine (hamkorlik qiluvchi funksiya) deb ataladi: bajarilish tashqi kod bilan Generator o'rtasida navbatma-navbat o'tadi.


6. return Generator ichida — nima bo'ladi?

function* gen() {
  yield 1;
  yield 2;
  return 'yakuniy natija';
  yield 3; // hech qachon yetib bormaydi
}

const g = gen();
g.next(); // { value: 1, done: false }
g.next(); // { value: 2, done: false }
g.next(); // { value: 'yakuniy natija', done: true } ← return qiymati value bo'ladi!
g.next(); // { value: undefined, done: true } — Generator butunlay tugagan

Muhim: return natijasi oxirgi { value, done: true } obyektining valuesi bo'ladi. Lekin oldingi mavzuda ko'rganimizdek — for...of va spread done: true bo'lgandagi valueni e'tiborsiz qoldiradi:

function* gen() {
  yield 1;
  yield 2;
  return 'yo\'qoladigan qiymat';
}

[...gen()]; // [1, 2] — return qiymati YO'Q!
for (const x of gen()) console.log(x); // faqat 1, 2

Bu — ko'p dasturchini hayratda qoldiradigan xatti-harakat. return qiymatini olish uchun .next()ni qo'lda chaqirish kerak, for...of bilan emas.


7. .return() va .throw() — Generator'da amalda

Bu — oldingi mavzuda "ixtiyoriy metodlar" deb o'rgangan narsalarning Generator'dagi haqiqiy kuchi.

.return() — majburiy erta to'xtatish

function* gen() {
  try {
    yield 1;
    yield 2;
    yield 3;
  } finally {
    console.log('Tozalash bajarildi!');
  }
}

const g = gen();
g.next();      // { value: 1, done: false }
g.return('majburiy tugatish');
// 'Tozalash bajarildi!' chop etiladi
// { value: 'majburiy tugatish', done: true }

.return() chaqirilganda, Generator ichida xuddi shu nuqtada return operatori yozilgandek ishlaydi — shuning uchun try/finally bloklari kafolatlangan holda ishga tushadi. Bu — resurslarni to'g'ri tozalash (fayl yopish, connection uzish, subscription bekor qilish) uchun juda muhim.

break/for...of bilan avtomatik bog'liqlik (oldingi mavzudan eslang):

function* readLines() {
  try {
    yield 'qator 1';
    yield 'qator 2';
    yield 'qator 3';
  } finally {
    console.log('Fayl yopildi');
  }
}

for (const line of readLines()) {
  console.log(line);
  if (line === 'qator 2') break;
}
// 'qator 1'
// 'qator 2'
// 'Fayl yopildi'   ← for...of ichidagi break avtomatik .return()ni chaqiradi!

.throw() — Generator ichiga xato inject qilish

function* gen() {
  try {
    console.log('boshlanish');
    yield 1;
    console.log('bu qatorga yetmaydi');
  } catch (err) {
    console.log('Generator ichida ushlandi:', err.message);
  }
}

const g = gen();
g.next();                       // 'boshlanish' → { value: 1, done: false }
g.throw(new Error('tashqi xato'));
// 'Generator ichida ushlandi: tashqi xato'
// { value: undefined, done: true }

Xato yield turgan aynan o'sha nuqtada "tashlangandek" ishlaydi — Generator'ning ichki try/catch shu joyda ushlaydi. Bu — Generator asosida qurilgan eski co/redux-saga kabi kutubxonalarning asosiy mexanizmi bo'lgan (async/await kelishidan oldin).


8. yield* — Generator delegatsiyasi

yield* — boshqa iterable/Generator'ga butun iteratsiyani topshirish:

function* inner() {
  yield 'a';
  yield 'b';
}

function* outer() {
  yield 1;
  yield* inner(); // inner()ning HAMMA yield'larini o'tkazadi
  yield 2;
}

[...outer()]; // [1, 'a', 'b', 2]

yield* inner() — bu qisqartma yozuv, aslida quyidagiga teng (spec darajasida):

function* outer() {
  yield 1;
  for (const val of inner()) {
    yield val;
  }
  yield 2;
}

Lekin oddiy for...of bilan farqi bor — yield* shunchaki qiymatlarni o'tkazib qolmaydi, balki .next(value), .return(), .throw() chaqiruvlarini ham to'g'ridan-to'g'ri ichki Generator'ga uzatadi:

function* inner() {
  const x = yield 'ichkaridan so\'rov';
  console.log('inner oldi:', x);
}

function* outer() {
  yield* inner();
}

const g = outer();
g.next();          // { value: 'ichkaridan so\'rov', done: false }
g.next('tashqi javob'); // outer orqali to'g'ridan-to'g'ri inner ga o'tadi!
// 'inner oldi: tashqi javob'

Bu — nega yield* "delegatsiya" deb atalishining sababi: outer butunlay shaffof vositachi bo'lib qoladi, innerning to'liq ikki tomonlama protokoli (.next(), .return(), .throw()) saqlanib qoladi.

yield* har qanday iterable bilan ishlaydi, faqat Generator bilan emas:

function* gen() {
  yield* [1, 2, 3];       // Array iterable
  yield* 'ab';             // String iterable
  yield* new Set([4, 5]);  // Set iterable
}

[...gen()]; // [1, 2, 3, 'a', 'b', 4, 5]

9. V8 ichida Generator qanday amalga oshiriladi

Bu — eng chuqur va eng qiziqarli qism. Savol: oddiy funksiya "pauza qilib, keyin davom eta olishi" uchun V8 nima qiladi?

Oddiy funksiya chaqirilganda uning local o'zgaruvchilari, execution pointer — bularning hammasi Call Stackda yashaydi (avvalgi bosqichlarda ko'rgan edik). Funksiya tugaganda, uning stack frame'i yo'q qilinadi. Agar Generator ham xuddi shunday ishlasa, pauza qilingandan keyin davom etish uchun stack frame saqlanib qolishi kerak edi — lekin stack bunday ishlamaydi (LIFO tartib, boshqa chaqiruvlar ustiga tushadi).

V8 yechimi: Generator uchun state Call Stack'da emas, Heap'da saqlanadi.

Aniqrog'i, V8 Generator funksiyani compile qilish vaqtida butunlay boshqacha strukturaga aylantiradi — bu jarayon "state machine transformation" deb ataladi (ba'zi engine/compiler'larda "CPS — Continuation Passing Style" ga o'xshash yondashuv).

Kontseptual model

Generator funksiya kompilyatsiya vaqtida ichki holat mashinasi (state machine)ga aylantiriladi. Har yield — bu "holat chegarasi":

function* gen() {
  console.log('A');
  yield 1;
  console.log('B');
  yield 2;
  console.log('C');
}

V8 buni kontseptual jihatdan (soddalashtirilgan, haqiqiy V8 bytecode emas, tushunish uchun model) shunga o'xshash narsaga aylantiradi:

function genStateMachine(state, sentValue) {
  switch (state.step) {
    case 0:
      console.log('A');
      state.step = 1;
      return { value: 1, done: false };
    case 1:
      console.log('B');
      state.step = 2;
      return { value: 2, done: false };
    case 2:
      console.log('C');
      state.step = 3;
      return { value: undefined, done: true };
  }
}

state obyekti — bu Generator Objectning o'zi, va u Heap'da yashaydi (stack'da emas!). Har .next() chaqirilganda, V8 shunchaki shu state.stepga qarab qayerdan davom etishni biladi va kodni o'sha nuqtadan bajaradi.

Haqiqiy V8 mexanizmi — Generator Object va Context

V8 (Ignition bytecode interpreter darajasida) har bir Generator chaqiruvi uchun JSGeneratorObject deb nomlangan maxsus obyekt yaratadi. Bu obyekt saqlaydi:

  1. [[GeneratorState]] — suspendedStart, suspendedYield, executing, completed kabi holatlar
  2. [[GeneratorContext]] — bu Generator'ning barcha lokal o'zgaruvchilarini saqlovchi Context obyekt (Closure mavzusida ko'rgan Context'ga o'xshash — heap'da)
  3. Continuation/resume point — bytecode'da qaysi "offset"dan davom etish kerakligi (yield operatoriga mos bytecode manzili)
JSGeneratorObject (heap):
  ┌─────────────────────────────┐
  │ state: 'suspendedYield'     │
  │ context: { local vars... }  │  ← heap dagi Context obyekti
  │ resumePoint: bytecode#42    │  ← qaysi yield'dan keyin davom etish
  │ receiver: this qiymati      │
  └─────────────────────────────┘

.next() chaqirilganda:

  1. V8 JSGeneratorObjectning holatini executingga o'zgartiradi
  2. Saqlangan Context'ni qayta faollashtiradi (o'zgaruvchilar qiymati saqlanib qolgan)
  3. Bytecode'ni resumePointdan boshlab bajaradi
  4. Keyingi yieldga (yoki returnga) yetganda, yangi resumePoint saqlanadi, holat qayta suspendedYieldga o'tadi, va { value, done } qaytariladi

Bu — nega Generator'ning lokal o'zgaruvchilari .next() chaqiruvlari orasida "unutilmasligi"ning texnik sababi: ular oddiy stack frame'da emas, balki Generator Object ichidagi, heap'dagi Context'da yashaydi, va bu obyekt Generator "tirik" ekan xotirada saqlanadi.


10. Xotira va Garbage Collection nuqtai nazaridan

Oddiy funksiya chaqiruvi tugagach, uning stack frame'i darhol bo'shatiladi (arzon, tez). Generator esa — hech qachon .next() orqali oxirigacha ishlatilmasa yoki .return() chaqirilmasa/GC qilinmasa — uning JSGeneratorObjecti (va u bilan bog'liq Context, ya'ni barcha lokal o'zgaruvchilar) heap'da tirik qolaveradi:

function* infiniteGen() {
  let bigData = new Array(1000000).fill('x'); // katta massiv
  while (true) {
    yield bigData.length;
  }
}

const g = infiniteGen();
g.next(); // Generator "pauza"da, lekin bigData hali xotirada — GC uni tozalay olmaydi!

Agar bu Generator hech qachon to'liq ishlatilmasa yoki .return() chaqirilmasa, va uning referensi (masalan global o'zgaruvchida) saqlanib qolsa — bu memory leak manbai bo'lishi mumkin. Shuning uchun uzoq muddat ishlatilmaydigan Generator'larni ochiq qoldirmaslik, kerak bo'lmasa .return() chaqirish yaxshi amaliyot hisoblanadi.


11. Generator'ning amaliy qo'llanilishi — cheksiz (lazy) ketma-ketliklar

function* naturals() {
  let n = 1;
  while (true) yield n++;
}

function* take(iterable, n) {
  let i = 0;
  for (const val of iterable) {
    if (i++ >= n) return;
    yield val;
  }
}

[...take(naturals(), 5)]; // [1, 2, 3, 4, 5]

Butun cheksiz massivni xotirada saqlamasdan, faqat kerakli qismini hisoblaydi — lazy evaluation.


12. Generator vs oddiy funksiya — yakuniy taqqoslash

Oddiy funksiya Generator
Chaqirilganda Darhol to'liq bajariladi Faqat Generator Object yaratadi, kod ishlamaydi
Qaytarish Bitta return Ko'p marta yield, oxirida return
Holat saqlash Stack frame (vaqtinchalik) Heap'dagi Context (Generator "tirik" ekan saqlanadi)
Pauza qila oladimi Yo'q Ha — yield orqali
Tashqaridan qiymat qabul qiladimi Faqat chaqirilganda (argument) Har .next(value) da, ishlash jarayonida ham
Xato ushlash Oddiy try/catch try/catch + tashqaridan .throw() orqali ham

Xulosa

Generator — Iterable va Iterator Protocol'larini qo'lda yozish o'rniga, function*/yield sintaksisi orqali til darajasida avtomatik yaratib beruvchi mexanizm: Generator Object bir vaqtning o'zida ham Iterator (.next()), ham Iterable ([Symbol.iterator]() — o'zini qaytaradi, "self-iterable") bo'lgani uchun for...of, spread va destructuring bilan qo'shimcha kodsiz ishlaydi. yield ifoda sifatida ikki tomonlama aloqani ta'minlaydi — .next(value) orqali Generator ichiga qiymat yuborish mumkin, .return() try/finally orqali cleanup'ni kafolatlaydi, .throw() esa xatoni aynan yield turgan nuqtada inject qiladi, yield* esa bu uch metodni ham shaffof tarzda ichki Generator'ga delegatsiya qiladi. V8 buni Call Stack orqali emas, compile vaqtida holat mashinasi (state machine)ga aylantirib, har bir chaqiruv uchun heap'da JSGeneratorObject (holat, Context, resume point) sifatida saqlaydi — shu sababli Generator'ning lokal o'zgaruvchilari .next() chaqiruvlari orasida yo'qolmaydi, lekin ayni sababdan tugallanmagan yoki referensi saqlanib qolgan Generator'lar GC tomonidan tozalanmay, memory leak manbaiga aylanishi mumkin.