Series: JavaScript Basics Lesson 40

Iterator Protocol

Iterator Protocol'ning aniq spec ta'rifi, IteratorResult semantikasi, .next(value) orqali qiymat yuborish, ixtiyoriy .return()/.throw() metodlari va V8 ichida Iterator qanday amalga oshirilishi (shu jumladan iterator elision).

40-dars

Iterator Protocol

1. Kirish — Iterable'dan Iterator'ga

Oldingi mavzuda Iterable Protocolni ko'rdik — "men iteratsiya qilinishim mumkin" deb e'lon qilish shartnomasi. Endi ikkinchi, undan ham muhim qismga o'tamiz: Iterator Protocol — haqiqiy iteratsiya jarayonining o'zi qanday ishlashini belgilaydigan shartnoma.

Bu ikkisini alohida o'rganish muhim, chunki ko'p dasturchi ularni bitta narsa deb o'ylaydi. Aslida: Iterable — "meni qanday iteratsiya qilish kerakligini bilaman", Iterator — "men aynan iteratsiyani yurituvchi mexanizmman". Endi shu ikkinchisiga to'liq kiramiz.


2. Iterator Protocol — aniq spec ta'rifi

Spec bo'yicha: Object Iterator hisoblanadi, agar unda next nomli, argumentsiz (yoki bitta argument bilan) chaqirilganda "Iterator Result" obyektini qaytaruvchi metod bo'lsa.

Formula:

obj.next  // funksiya bo'lishi shart
obj.next() // chaqirilganda { value, done } shaklidagi obyekt qaytarishi shart

Shunchaki shu. Symbol.iterator shart emas (garchi odatda bo'ladi — buni pastroqda ko'ramiz). Faqat .next() bo'lsa yetarli, u haqiqiy protokolga rioya qiladi.

// Eng minimal Iterator
const counter = {
  count: 0,
  next() {
    this.count++;
    return this.count <= 3
      ? { value: this.count, done: false }
      : { value: undefined, done: true };
  }
};

counter.next(); // { value: 1, done: false }
counter.next(); // { value: 2, done: false }
counter.next(); // { value: 3, done: false }
counter.next(); // { value: undefined, done: true }

Bu — Iterator, lekin Iterable emas (chunki Symbol.iterator yo'q). Shuning uchun for...of bilan to'g'ridan-to'g'ri ishlatib bo'lmaydi:

for (const x of counter) { }
// TypeError: counter is not iterable

Bu — ikkalasini chalkashtirmaslik nima uchun muhimligini ko'rsatuvchi eng aniq misol.


3. IteratorResult obyekti — ikkita maydonning aniq semantikasi

Har .next() chaqiruvi yangi { value, done } obyekt qaytarishi kerak (spec bo'yicha, garchi ba'zi implementatsiyalar bir xil obyektni qayta ishlatishi mumkin — lekin bu tavsiya etilmaydi).

  • done: false — hali qiymatlar bor, davom eting.
  • done: true — iteratsiya tugadi, value odatda undefined (lekin ba'zan Generator'da "return qiymati" bo'lishi mumkin — buni Generator mavzusida ko'ramiz).

Muhim nozik tafsilot — done: true bo'lgandagi valueni e'tiborsiz qoldirish kerak deb hisoblanadi ko'p consumer (for...of, spread) tomonidan:

const it = {
  i: 0,
  next() {
    this.i++;
    if (this.i === 1) return { value: 'a', done: false };
    if (this.i === 2) return { value: 'oxirgi qiymat', done: true }; // e'tibor bering!
    return { value: undefined, done: true };
  }
};

[...{ [Symbol.iterator]: () => it }]; // ['a'] — 'oxirgi qiymat' TASHLAB KETILADI!

Chunki for...of/spread spec algoritmi: .next() chaqiradi → done tekshiradi → agar true bo'lsa, value'ni butunlay e'tiborsiz qoldirib, tsiklni to'xtatadi. Bu — Generator'ning return qiymati nega for...of ichida "yo'qoladi" degan savolga ham javob (Generator mavzusida chuqurroq ko'ramiz).


4. .next(value) — Iterator'ga qiymat yuborish

Bu qism kam ma'lum, lekin muhim: .next() argument qabul qilishi ham mumkin. Oddiy Array/String Iterator'lari buni e'tiborsiz qoldiradi, lekin Generator'lar buni faol ishlatadi (bu — Generator'ning eng kuchli xususiyati, "ikki tomonlama aloqa"):

function* echoGenerator() {
  while (true) {
    const received = yield; // yuborilgan qiymatni qabul qiladi
    console.log('Oldim:', received);
  }
}

const gen = echoGenerator();
gen.next();          // birinchi next() — hali "received"gacha yetib bormaydi
gen.next('salom');   // Oldim: salom
gen.next('yana');    // Oldim: yana

Bu — Iterator Protocol'ning oddiy Iterator'larda deyarli ishlatilmaydigan, lekin Generator orqali kuchga kiradigan qismi. Buni Generator mavzusida to'liq ochamiz, hozircha shuni bilish kifoya: .next(arg) — protokolning rasmiy qismi, oddiy iteratorlar buni shunchaki e'tiborsiz qoldiradi.


5. Ixtiyoriy metodlar — .return() va .throw()

Iterator Protocol'da .next() majburiy, lekin ikkita ixtiyoriy metod ham bor: .return() va .throw(). Bular "to'liq protokol"ni tashkil qiladi va ko'p dasturchi ulardan bexabar.

.return() — erta to'xtatish signali

Agar iteratsiya tugamasdan to'xtatilsa (masalan, break, return, yoki xato tufayli), consumer (masalan for...of) Iterator'da .return() metodi bormi tekshiradi va bo'lsa, uni chaqiradi — bu Iterator'ga "tozalash ishlarini bajar" (cleanup) signalidir:

function makeRangeIterator(start, end) {
  let current = start;
  return {
    next() {
      return current <= end
        ? { value: current++, done: false }
        : { value: undefined, done: true };
    },
    return(value) {
      console.log('Tozalash ishga tushdi!');
      return { value, done: true }; // .return() ham IteratorResult qaytarishi shart
    }
  };
}

const iterable = {
  [Symbol.iterator]() { return makeRangeIterator(1, 10); }
};

for (const n of iterable) {
  console.log(n);
  if (n === 3) break; // erta to'xtash
}
// 1
// 2
// 3
// Tozalash ishga tushdi!

break chaqirilganda, V8 avtomatik ravishda .return()ni chaqiradi — buni dasturchi qo'lda yozmaydi, bu spec darajasidagi kafolat. Aynan shu mexanizm orqali fayl handle, network connection, yoki resurs ochib turgan Iterator'lar for...of erta to'xtaganda ham to'g'ri yopilishini kafolatlaydi:

function* readFileLines(path) {
  const file = openFile(path); // faraziy
  try {
    for (const line of file) yield line;
  } finally {
    file.close(); // bu HAR DOIM ishlaydi — break, return, throw bo'lsa ham!
  }
}

Generator'larda bu avtomatik try/finally orqali ta'minlanadi — .return() chaqirilganda Generator ichidagi finally bloki ishga tushadi. Bu — Generator'ning Iterator Protocol ustiga qurgan eng katta amaliy foydasi.

Qachon .return() chaqirilmaydi: agar tsikl to'liq tugasa (barcha elementlar done: true bilan yakunlansa), .return() chaqirilmaydi — chunki Iterator o'zi allaqachon tugagan, tozalashga hojat yo'q.

.throw() — Iterator ichiga xato yuborish

.throw() — Generator kontekstida eng foydali, lekin rasman umumiy Iterator Protocol'ning qismi. U Iterator ichiga xato inject qilish imkonini beradi:

function* gen() {
  try {
    yield 1;
    yield 2;
  } catch (err) {
    console.log('Generator ichida ushlandi:', err.message);
    yield 'xatodan keyingi qiymat';
  }
}

const g = gen();
g.next();               // { value: 1, done: false }
g.throw(new Error('tashqi xato'));
// Generator ichida ushlandi: tashqi xato
// { value: 'xatodan keyingi qiymat', done: false }

Bu — xatoni Generator funksiyani chaqirgan joyda emas, balki yield turgan aynan o'sha nuqtada ushlanishini ta'minlaydi. Oddiy Iterator'larda .throw() deyarli hech qachon implementatsiya qilinmaydi, chunki ularda "pauza nuqtasi" degan tushuncha yo'q.

To'liq Iterator interfeysi:

Metod Majburiymi? Vazifasi
.next(value?) ✅ Ha Keyingi qiymatni beradi, ixtiyoriy valueni Generator ichiga yuboradi
.return(value?) ❌ Ixtiyoriy Erta to'xtaganda cleanup signali, { value, done: true } qaytarishi kerak
.throw(err) ❌ Ixtiyoriy Iterator ichiga xato inject qiladi (Generator uchun eng foydali)

6. V8 ichida Iterator qanday amalga oshiriladi

Oddiy (qo'lda yozilgan) Iterator uchun: bu — oddiy JS obyekti, .next — oddiy metod. V8 uni boshqa har qanday object metod chaqiruvi kabi ko'radi: Inline Cache (IC) orqali optimallashadi, agar bir xil "shape" (Hidden Class) bo'lgan Iterator'lar ketma-ket chaqirilsa (masalan tsikl ichida), V8 tezda monomorphic holatga o'tadi va chaqiruv deyarli bepul bo'lib qoladi.

Built-in Iterator'lar uchun (Array, String, Map, Set): V8 bularni C++ darajasida maxsus internal class sifatida amalga oshiradi (JSArrayIterator, JSMapIterator va h.k.). Bular JS obyekti ko'rinishida (.next chaqirsa bo'ladi) lekin haqiqatda ular ichida index/pointerni to'g'ridan-to'g'ri C++ struct ichida saqlaydi — bu JS property lookup'dan tezroq.

const arr = [1, 2, 3];
const it = arr[Symbol.iterator](); // JSArrayIterator — maxsus internal turi

it[Symbol.toStringTag]; // 'Array Iterator' — bu buni ko'rsatadi

Muhim optimallashtirish — "iterator elision" (bypass): Agar V8 for...of arr ni ko'rib, arr — oddiy, o'zgartirilmagan Array ekanini va Symbol.iterator hech qayerda override qilinmaganini bilsa, u haqiqiy Iterator obyektini yaratmasdan, to'g'ridan-to'g'ri ichki indexlash tsikliga o'tkazadi (JIT optimallashtirish, "TurboFan" bosqichida). Bu — nega for...of array bilan deyarli klassik for tsikli kabi tez ishlaydi, degan savolga javob. Lekin bu optimallashtirish spekulyativ — agar runtime'da Symbol.iterator o'zgartirilgani sezilsa, V8 deoptimize qilib, sekin, umumiy protokol yo'liga qaytadi.


7. Iterator Protocol xatolari — spec qanday tekshiradi

const badIterator = { next: 'men funksiya emasman' };
const it = { [Symbol.iterator]: () => badIterator };

for (const x of it) { }
// TypeError: (intermediate value).next is not a function
const badResult = { next: () => 'string, obyekt emas' };
const it2 = { [Symbol.iterator]: () => badResult };

for (const x of it2) { }
// TypeError: Iterator result is not an object

Ikkala holatda ham V8 spec algoritmiga qat'iy rioya qiladi: .next funksiya bo'lishi, va uning natijasi obyekt bo'lishi shart.


Xulosa

Iterator Protocol — Iterable Protocol'dan alohida, lekin unga bog'liq shartnoma: object Iterator hisoblanishi uchun faqat .next() metodiga ega bo'lishi va u har chaqiruvda { value, done } shaklidagi obyekt qaytarishi kifoya — Symbol.iterator shart emas. done: true bo'lgan natijadagi value consumer'lar (for...of, spread) tomonidan e'tiborsiz qoldiriladi, .next(value) esa Iterator'ga (asosan Generator'larga) qiymat yuborish imkonini beradi. Ikkita ixtiyoriy metod — .return() (erta to'xtashda, masalan breakda, avtomatik chaqiriladigan cleanup hook) va .throw() (Iterator ichiga xato inject qilish, Generator'ning try/catchi bilan ishlaydi) — "to'liq protokol"ni tashkil qiladi va Generator'larning try/finally orqali resurslarni ishonchli yopishini ta'minlaydi. V8 built-in Iterator'larni (JSArrayIterator va h.k.) C++ darajasida tezlashtiradi va hatto ba'zi hollarda ("iterator elision") haqiqiy Iterator obyektini umuman yaratmasdan to'g'ridan-to'g'ri indexlash tsikliga o'tadi — lekin bu optimallashtirish Symbol.iterator override qilinishi bilan darhol bekor bo'ladi (deoptimization). Amaliyotda Iterator'lar odatda o'zlari ham [Symbol.iterator]() metodiga ega bo'lib, thisni qaytaradi — "self-iterable" konvensiyasi — biroq bu spec talabi emas, faqat qulaylik uchun qabul qilingan odat.