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
- 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.iteratoroverride qilinmagan bo'lsa. - 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.
- 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). - 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.