Series: JavaScript Basics Lesson 43

Destructuring, for...of, Spread/Rest

Array va Object destructuring nima uchun ikkita butunlay boshqa mexanizm, for...of va spread'ning Iterator Protocol bilan bog'liqligi, rest parametr, va V8 darajasida bularning har biri qanday optimallashtirilishi.

43-dars

Destructuring, for...of, Spread/Rest

1. Kirish — bitta protokolning uchta ko'rinishi

Bu uch mexanizmni birga o'rganish to'g'ri qaror — chunki ularning hammasi bitta umumiy asosga: Iterable/Iterator Protocol'ga (oldingi mavzularda to'liq ko'rgan edik) tayanadi. Ularni alohida-alohida o'rgansangiz, har birini "sehr" deb qabul qilib qolasiz. Birga ko'rsangiz — bitta protokolning uchta turli sintaktik ko'rinishi ekani ayon bo'ladi.


2. Destructuring — ikkita butunlay boshqa mexanizm

Bu yerdagi eng katta chalkashlik: Array destructuring va Object destructuring — bir xil ko'rinadi, lekin ichida butunlay boshqa ikki mexanizm ishlaydi. Buni ajratib olish kerak.

Array Destructuring — bu Iterator Protocol'ning o'zi

const [a, b, c] = [10, 20, 30];

Bu yerda hech qanday index bo'yicha o'qish yo'q. V8 buni aslida shunga aylantiradi:

const _iterator = arr[Symbol.iterator]();
const a = _iterator.next().value;
const b = _iterator.next().value;
const c = _iterator.next().value;

Isbot — index emas, Iterator Protocol ekanini tekshirish mumkin:

const fakeIterable = {
  [Symbol.iterator]() {
    let i = 0;
    return { next: () => ({ value: ['x', 'y', 'z'][i], done: i++ >= 3 }) };
  }
};

const [p, q, r] = fakeIterable; // 'x' 'y' 'z' — hech qanday .length yoki [0] yo'q!

Bu obyektda .length ham, index ham yo'q, faqat Symbol.iterator bor — shunga qaramay destructuring ishlayveradi. Demak: Array destructuring — Symbol.iterator protokoliga bog'liq, Array classiga emas.

Shuning uchun Array destructuring har qanday iterable bilan ishlaydi:

const [x, y] = 'hi';            // String iterable → x='h', y='i'
const [m, n] = new Set([1, 2]); // Set iterable → m=1, n=2
const [g1, g2] = (function*(){ yield 'a'; yield 'b'; })(); // Generator

Nozik nuqta — Map destructuring nega ishlaydi:

const map = new Map([['name', 'Ali'], ['age', 25]]);
for (const [key, value] of map) {
  console.log(key, value); // 'name' 'Ali', keyin 'age' 25
}

Map'ning Symbol.iteratori har elementni [key, value] ikki elementli Array shaklida qaytaradi (oldingi Map mavzusida ko'rgan edik). Shuning uchun for (const [key, value] of map) ichida ikki bosqichli destructuring sodir bo'ladi: avval for...of map'dan [key, value] juftligini oladi (tashqi Iterator), keyin shu juftlikning o'zi Array destructuring orqali key/value'ga ajratiladi (ichki Iterator, chunki [key, value] — Array, u ham iterable).

Object Destructuring — bu butunlay boshqa narsa: oddiy property lookup

const { name, age } = { name: 'Ali', age: 25 };

Bu — Iterator Protocol bilan hech qanday aloqasi yo'q. Bu shunchaki:

const name = obj.name;
const age = obj.age;

ning qisqartmasi. V8 buni to'g'ridan-to'g'ri Hidden Class orqali offset lookup qiladi — xuddi oddiy .name murojaati kabi, hech qanday Iterator, hech qanday Symbol.iterator chaqiruvi yo'q.

const obj = { a: 1, b: 2 };
// obj[Symbol.iterator] yo'q, lekin:
const { a, b } = obj; // ishlaydi! chunki bu oddiy property lookup

Agar Object destructuring Iterator Protocol'ga bog'liq bo'lganida, oldingi mavzuda ko'rganimizdek {} iterable emasligi sababli xato berardi. Lekin bermaydi — chunki Object destructuring va Array destructuring ikkita mutlaqo boshqa spec algoritmiga tayanadi:

Array destructuring [a,b] Object destructuring {a,b}
Asos Iterator Protocol (Symbol.iterator, .next()) Oddiy property access ([[Get]])
Nima bilan ishlaydi Har qanday iterable Har qanday object (property'lari bo'lsa)
V8 optimallashtirish Iterator chaqiruv zanjiri (yoki elision fast-path) Hidden Class offset lookup (juda tez)

Default qiymatlar va nested destructuring

const { name = 'Noma\'lum', address: { city } = {} } = { name: 'Ali' };
// name = 'Ali', city = undefined (chunki address yo'q, {} default ishlatildi)

Default qiymat (= 'Noma\'lum') faqat va faqat qiymat undefined bo'lganda ishga tushadi (null emas!):

const { x = 10 } = { x: null };
console.log(x); // null — default ISHLAMAYDI, chunki null !== undefined

Bu — V8 ichida oddiy void 0 (undefined) tekshiruvi, spec algoritmi aniq shunday belgilangan: if (value === undefined) value = defaultExpr.

Array destructuring bilan skip va rest

const [, second, , fourth] = [1, 2, 3, 4];
// second=2, fourth=4

Bo'sh vergul — bu _iterator.next() shunchaki chaqiriladi, lekin natija hech qayerga yozilmaydi degani. Iteratsiya baribir davom etadi (bu muhim — agar Iterator'da side-effect bo'lsa, u baribir ishga tushadi):

let calls = 0;
const gen = (function*() { while(true) { calls++; yield calls; } })();
const [, b] = gen;
console.log(calls); // 2 — birinchi next() ham chaqirilgan, faqat natija tashlab yuborilgan

3. for...of — Iterator Protocol'ning tsikl ko'rinishi

Oldingi mavzuda buni to'liq ochgan edik: for...of — Iterator Protocol'ning tsikl ko'rinishi:

for (const x of iterable) { }

// ↓ V8 buni shunga aylantiradi:
const it = iterable[Symbol.iterator]();
let result = it.next();
while (!result.done) {
  const x = result.value;
  // tana
  result = it.next();
}

Bu yerda muhim yangi nuqta — for...of ichida destructuring bilan birlashish:

const entries = [['a', 1], ['b', 2]];

for (const [key, val] of entries) {
  console.log(key, val); // 'a' 1, 'b' 2
}

Bu — ikki bosqichli protokol ishlashi: for...of tashqi entries massividan har elementni .next() orqali oladi (bosqich 1 — Iterator Protocol), keyin har element (['a', 1]) o'zi ham Array bo'lgani uchun [key, val] orqali yana Iterator Protocol orqali ochiladi. Ikkalasi ham — bitta protokolning ikki marta qo'llanilishi.


4. Spread (...) — Iterator Protocol'ning "yig'uvchi" ko'rinishi

Spread — ikkita kontekstda ishlatiladi, va ular turlicha ishlaydi: iterable spread (Array/function chaqiruv ichida) va object spread ({...obj}). Bu — Destructuring'dagi xuddi shu ikkiga bo'linish, faqat teskari yo'nalishda (yig'ish, ajratish emas).

Iterable Spread — Iterator Protocol orqali

const arr = [1, 2, 3];
const copy = [...arr];

V8 buni kontseptual jihatdan shunga aylantiradi:

const copy = [];
const it = arr[Symbol.iterator]();
let result = it.next();
while (!result.done) {
  copy.push(result.value);
  result = it.next();
}

Aynan for...of bilan bir xil mexanizm — farq shunda: for...of har elementni tashqi kodga beradi, spread esa har elementni yangi Array ichiga yig'adi. Ikkalasi ham xuddi bitta GetIterator + .next() tsikliga tayanadi.

function* gen() { yield 1; yield 2; yield 3; }
[...gen()]; // [1, 2, 3] — Generator ham to'liq "yig'iladi"

Math.max(...gen()); // 3 — function argument sifatida ham spread qilinadi

Muhim ogohlantirish — cheksiz Iterator bilan spread:

function* infinite() { let n = 0; while(true) yield n++; }
[...infinite()]; // Bu HECH QACHON tugamaydi! Xotira to'lguncha yoki freeze bo'lguncha davom etadi

Chunki spread .next()ni done: true bo'lguncha to'xtovsiz chaqiraveradi — bu Generator "cheksiz" bo'lsa, spread ham cheksiz davom etadi. for...of bilan break orqali to'xtatish mumkin edi, lekin spread'da bunday "erta to'xtatish" imkoni yo'q — u har doim to'liq tugashini talab qiladi.

Object Spread — Iterator Protocol'ga umuman bog'liq emas

const obj = { a: 1, b: 2 };
const copy = { ...obj, c: 3 };

Bu — Object destructuring kabi, Iterator Protocol'ga umuman bog'liq emas. V8 buni [[OwnPropertyKeys]] + har birini [[Get]] + yangi obyektga [[Set]] ketma-ketligiga aylantiradi — bu, mohiyatan, Object.assign({}, obj, {c: 3})ning sintaktik ekvivalenti:

// Kontseptual ekvivalent:
const copy = {};
for (const key of Reflect.ownKeys(obj)) {
  const desc = Object.getOwnPropertyDescriptor(obj, key);
  if (desc.enumerable) copy[key] = obj[key];
}
copy.c = 3;

Shuning uchun {...obj} faqat enumerable own property'larni ko'chiradi (prototype'dagi property'larni emas — chunki for...indan farqli, [[OwnPropertyKeys]] faqat o'zining property'larini beradi):

class Base { constructor() { this.x = 1; } }
class Sub extends Base { constructor() { super(); this.y = 2; } }
Sub.prototype.z = 3; // prototype'ga qo'shildi, own emas

const s = new Sub();
const copy = { ...s };
copy; // { x: 1, y: 2 } — z YO'Q, chunki own property emas

Object spread — shallow copy ekanini yodda tuting.


5. Rest — spread'ning "teskarisi", destructuring ichida

const [first, ...rest] = [1, 2, 3, 4];
// first=1, rest=[2, 3, 4]

const { a, ...others } = { a: 1, b: 2, c: 3 };
// a=1, others={b:2, c:3}

function f(first, ...args) { }
// args — chaqiruvda berilgan qolgan argumentlar Array'i

Rest — spread bilan bir xil ... belgisi, lekin teskari vazifa: spread yoyadi (bitta iterable'ni ko'p elementga), rest yig'adi (ko'p elementni bitta Array/Object'ga). Qaysi biri ekanligi kontekstga qarab aniqlanadi:

[...arr]        // spread — arr yoyilmoqda
[a, ...rest]    // rest — qolganlari yig'ilmoqda

Array rest — Array destructuring bo'lgani uchun, u ham Iterator Protocol orqali ishlaydi (qolgan barcha .next() natijalarini yig'ib, yangi Array yaratadi). Object rest — Object destructuring bo'lgani uchun, u ham oddiy property yig'ish orqali ishlaydi.

arguments obyekti bilan solishtirish (eski uslub, Function/Closure mavzusidan eslang):

function old() {
  console.log(arguments); // array-like, lekin haqiqiy Array emas, .map() yo'q
}

function modern(...args) {
  console.log(args); // haqiqiy Array — .map(), .filter() ishlaydi
}

Rest parametr V8 ichida haqiqiy Array sifatida yaratiladi (argumentsdan farqli, u maxsus "array-like" internal obyekt edi) — bu zamonaviy kodda rest parametr afzal ko'rilishining sababi.


6. Uchalasini bog'lovchi yagona sxema

                    Symbol.iterator PROTOKOLI
                            │
        ┌───────────────────┼───────────────────┐
        ▼                    ▼                    ▼
  Array Destructuring    for...of            Spread (iterable)
  [a, b] = iterable      TSIKL orqali        [...iterable]
  .next() ni QO'LDA      .next() ni          .next() ni TO'LIQ
  N marta chaqiradi      done=true gacha     done=true gacha
                          chaqiradi            chaqiradi, natijani
                                                YANGI Array'ga yig'adi


                    ODDIY PROPERTY ACCESS ([[Get]]/[[OwnPropertyKeys]])
                            │
        ┌───────────────────┴───────────────────┐
        ▼                                         ▼
  Object Destructuring                      Object Spread
  {a, b} = obj                              {...obj}
  har key uchun obj.key                     har own-enumerable key uchun
                                             yangi obj ga nusxa

Eng muhim xulosa: [...] bilan yoziladigan narsa (Array destructuring, iterable spread, Array rest) — hammasi Iterator Protocol'ga tayanadi, ya'ni Symbol.iterator va .next() chaqiriladi. {...} bilan yoziladigan narsa (Object destructuring, object spread, object rest) — Iterator bilan hech qanday aloqasi yo'q, oddiy property enumeration.


7. V8 optimallashtirish — amaliyot uchun muhim xulosalar

  1. Array destructuring/spread built-in Array bilan — V8 "iterator elision" (Iterator Protocol mavzusida ko'rgan edik) orqali buni inline qiladi, deyarli oddiy indexlash tezligida ishlaydi, agar Symbol.iterator override qilinmagan bo'lsa.
  2. Object destructuring/spread — bu har doim tezroq, chunki Iterator chaqiruv overhead'i umuman yo'q, faqat Hidden Class orqali to'g'ridan-to'g'ri offset lookup.
  3. Custom Iterable (Generator, class) bilan destructuring/spread — bu har doim haqiqiy funksiya chaqiruvlari zanjiri orqali o'tadi (.next() har safar chaqiriladi), Inline Cache monomorphic bo'lolmaydi to'liq — shu sababli built-in turlarga nisbatan sezilarli sekinroq, lekin bu deyarli hech qachon amaliy muammo emas (faqat juda katta hajmda, hot loop ichida ahamiyatli bo'lishi mumkin).
  4. Chuqur nested destructuring (const { a: { b: { c } } } = obj) — har daraja alohida [[Get]] chaqiruvi, V8 buni bosqichma-bosqich bajaradi, "sehrli" bitta amal emas.

Xulosa

Array destructuring, for...of va iterable spread — uchalasi ham bitta umumiy mexanizmga, Iterator Protocol'ga (Symbol.iterator + .next()) tayanadi, shuning uchun har qanday iterable (String, Set, Map, Generator, custom class) bilan bir xil ishlaydi; ular farqlanadi faqat qancha va qanday .next() chaqirishida — destructuring qo'lda N marta, for...of done: true gacha tsiklda, spread esa xuddi shunday tsiklda, lekin natijani yangi Array'ga yig'ib. Object destructuring, object spread va object rest esa bu protokolga umuman bog'liq emas — ular oddiy [[Get]]/[[OwnPropertyKeys]] orqali own-enumerable property'larni o'qiydi, shuning uchun Hidden Class orqali tezroq ishlaydi va Symbol.iteratori yo'q oddiy obyektlar bilan ham muammosiz ishlaydi. Bu ikki mustaqil mexanizmni bir xil [a,b]/{a,b} sintaksisi ortida yashirilgani — JavaScript'dagi eng ko'p chalkashtiriladigan, lekin protokol darajasida tushunilsa aslida juda izchil bo'lgan dizayn qarorlaridan biri.