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:
[[GeneratorState]]—suspendedStart,suspendedYield,executing,completedkabi holatlar[[GeneratorContext]]— bu Generator'ning barcha lokal o'zgaruvchilarini saqlovchi Context obyekt (Closure mavzusida ko'rgan Context'ga o'xshash — heap'da)- Continuation/resume point — bytecode'da qaysi "offset"dan davom etish kerakligi (
yieldoperatoriga 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:
- V8
JSGeneratorObjectning holatiniexecutingga o'zgartiradi - Saqlangan Context'ni qayta faollashtiradi (o'zgaruvchilar qiymati saqlanib qolgan)
- Bytecode'ni
resumePointdan boshlab bajaradi - Keyingi
yieldga (yokireturnga) yetganda, yangiresumePointsaqlanadi, holat qaytasuspendedYieldga 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.