Series: JavaScript Basics Lesson 38

Iterator

Iteration Protocol nima, Iterable va Iterator o'rtasidagi farq, for...of ichida V8 aslida nima qilishi, custom Iterable yasash, V8 optimallashtirishlari va Iterator Helpers (ES2024).

38-dars

Iterator

1. Muammo — nima uchun Iterator kerak bo'ldi?

ES6 gacha JS da "collection ustidan yurish" degan yagona standart yo'l yo'q edi. Array uchun for, object uchun for...in, boshqa custom struktura (masalan, linked list, tree) uchun har kim o'zicha metod yozardi. Bir xil interfeys yo'q edi.

// Array
for (let i = 0; i < arr.length; i++) { }

// Object
for (let key in obj) { }

// Custom struktura?
// Har kim o'z usulida — standart yo'q edi

ES6 Iteration Protocol deb nomlangan umumiy "shartnoma" kiritdi: agar biror narsa shu shartnomaga rioya qilsa, u for...of, spread (...), destructuring, Array.from() kabi barcha til mexanizmlari bilan avtomatik ishlaydi.

Bu protokol ikkita alohida tushunchadan iborat, va aynan shu ikkitasini chalkashtirmaslik muhim: Iterable va Iterator.


2. Iterable vs Iterator — asosiy farq

Iterable — bu Symbol.iterator metodiga ega bo'lgan har qanday obyekt. U "meni qanday iteratsiya qilish kerakligini bilaman" deydi.

Iterator — bu .next() metodiga ega bo'lgan obyekt. U "men haqiqiy iteratsiya jarayonini yurituvchi kursor"man deydi.

const arr = [10, 20, 30];

typeof arr[Symbol.iterator]; // 'function' — arr Iterable

const it = arr[Symbol.iterator](); // Iterable → Iterator yaratadi
typeof it.next; // 'function' — it Iterator

it.next(); // { value: 10, done: false }
it.next(); // { value: 20, done: false }
it.next(); // { value: 30, done: false }
it.next(); // { value: undefined, done: true }

Munosabat shunday: Iterable — fabrika, Iterator — mahsulot. Symbol.iterator chaqirilganda yangi Iterator obyekti yaratiladi, va o'sha Iterator o'zining ichki kursor holati (state)'ni saqlaydi (qaysi elementda turganini).

Shu sababdan bir Array'dan bir vaqtda ikkita mustaqil iteratsiya yuritish mumkin — chunki har [Symbol.iterator]() chaqiruvi yangi, mustaqil Iterator yaratadi:

const it1 = arr[Symbol.iterator]();
const it2 = arr[Symbol.iterator]();

it1.next(); // { value: 10, done: false }
it1.next(); // { value: 20, done: false }
it2.next(); // { value: 10, done: false } — it1 dan mustaqil!

3. for...of ichida V8 aslida nima qiladi?

for...of — bu shunchaki quyidagi kodning "syntactic sugar" (qulaylashtirilgan yozilishi):

for (const x of arr) {
  console.log(x);
}

V8 buni ichida shunga o'xshash bosqichlarga aylantiradi:

const iterator = arr[Symbol.iterator]();
let result = iterator.next();

while (!result.done) {
  const x = result.value;
  console.log(x);
  result = iterator.next();
}

Muhim nuqta: for...of hech qachon .length yoki index bilan ishlamaydi. U faqat Symbol.iterator protokoliga tayanadi. Shuning uchun for...of Array, String, Map, Set, NodeList, Generator — hammasida bir xil ishlaydi, ular hammasi shu bitta protokolga rioya qilgani uchun.

Muhim farq — nega oddiy Object for...of da ishlamaydi:

const obj = { a: 1, b: 2 };

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

Oddiy {} literal orqali yaratilgan object Symbol.iteratorga ega emas — chunki key-value juftlikni qanday "bitta qiymat" qilib qaytarish kerakligi noaniq (key? value? ikkalasi?). Shuning uchun JS dizayni ataylab oddiy Object'ni iterable qilmagan — for...in (key lar uchun) yoki Object.entries() (key-value juftlik uchun) ishlatishga majbur qiladi.


4. O'zingiz custom Iterable yasash

Bu joy — protokolni chuqur tushunish uchun eng yaxshi mashq. Aytaylik, biz Range obyekt yasaymiz — start dan end gacha sonlarni beradigan:

class Range {
  constructor(start, end) {
    this.start = start;
    this.end = end;
  }

  [Symbol.iterator]() {
    let current = this.start;
    const end = this.end;

    // Bu qaytarilayotgan obyekt — Iterator, .next() protokoliga mos
    return {
      next() {
        if (current <= end) {
          return { value: current++, done: false };
        }
        return { value: undefined, done: true };
      }
    };
  }
}

const range = new Range(1, 3);

for (const n of range) {
  console.log(n); // 1 2 3
}

[...range]; // [1, 2, 3] — spread ham shu protokolga tayanadi
const [a, b] = range; // destructuring ham shunday — a=1, b=2

E'tibor bering: next() metodi har chaqirilganda yangi obyekt ({ value, done }) qaytaradi. Bu — protokolning qat'iy talabi. V8 (va umuman spec) har .next() chaqiruvidan aynan shu shakldagi natija kutadi.


5. V8 ichida Iterator qanday optimallashtiriladi

Bu qism muhim — chunki "protokol" tushunchasi ba'zan "sekin bo'lishi kerak" degan noto'g'ri taassurot qoldiradi. Lekin V8 buni juda tez qiladi, chunki:

  1. Built-in iterator'lar uchun "fast path" bor. Array, String, Map, Set kabi built-in turlar uchun V8 Symbol.iterator chaqiruvini inline qiladi — ya'ni haqiqiy funksiya chaqiruvi (call overhead) bo'lmaydi, to'g'ridan-to'g'ri ichki C++ tsiklga aylantiriladi, agar dasturchi bu metodlarni qayta yozmagan (override qilmagan) bo'lsa.
for (const x of [1, 2, 3]) { }
// V8 buni deyarli oddiy `for (let i = 0; i < 3; i++)` kabi tez qiladi
  1. Agar siz Array.prototype[Symbol.iterator]ni o'zgartirsangiz, bu optimallashtirish yo'qoladi — V8 endi "polymorphic" holatga o'tadi va sekinroq, umumiy protokol yo'lidan yuradi. Shuning uchun built-in prototype'larni o'zgartirish (monkey-patching) performance uchun zararli bo'lishi mumkin.

  2. Custom Iterator (masalan yuqoridagi Range) uchun V8 har next() chaqiruvida oddiy metod chaqiruvi (megamorphic bo'lmasa — Inline Cache orqali tezlashadi) qiladi, va har safar { value, done } obyekti yaratiladi — bu kichik, qisqa umr ko'radigan obyekt, V8 ning Scavenger (Young Generation GC) buni juda tez tozalaydi, chunki bunday obyektlar odatda darhol "o'lik" bo'lib qoladi (faqat bitta iteratsiyada ishlatiladi).


6. Iterator Helpers (yangi, ES2024)

Yaqinda tilga Iterator'lar uchun to'g'ridan-to'g'ri metodlar qo'shildi (Array metodlariga o'xshash, lekin lazy — hisoblash faqat kerak bo'lganda amalga oshadi):

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

const first5EvenSquares = naturals()
  .filter(n => n % 2 === 0)
  .map(n => n * n)
  .take(5);

[...first5EvenSquares]; // [4, 16, 36, 64, 100]

Bu — Array.prototype.filter/map dan farqli, butun massivni xotirada yaratmaydi. Har qadam faqat bitta qiymatni "so'ralganda" hisoblaydi — bu Generator bilan chambarchas bog'liq (keyingi mavzu aynan shu).


Xulosa

Iteration Protocol — Iterable (Symbol.iterator metodiga ega obyekt, "fabrika") va Iterator (.next() metodiga ega, ichki kursor holatini saqlaydigan "mahsulot") degan ikki alohida tushunchadan iborat: Symbol.iterator chaqirilganda yangi, mustaqil Iterator yaratiladi, shuning uchun bitta Array'dan bir nechta parallel iteratsiya yuritish mumkin. for...of faqat shu protokolga tayanadi (.lengthga emas), shuning uchun Array, String, Map, Set va custom klasslar bir xil mexanizm bilan ishlaydi, oddiy {} object esa — key-value juftlikni bitta qiymatga aylantirish noaniq bo'lgani uchun — ataylab iterable emas. V8 built-in turlar uchun bu chaqiruvni inline qilib tezlashtiradi (agar prototype monkey-patch qilinmagan bo'lsa), custom Iterator'larda esa har .next() chaqiruvi qisqa umrli { value, done } obyekti yaratadi va Young Generation GC tomonidan tez tozalanadi. ES2024'dagi Iterator Helpers (filter, map, take va h.k.) esa shu protokol ustiga qurilgan, lazy hisoblaydigan yangi qatlam.