Prototype Chain
[[Prototype]] va __proto__ nima, property qidirish zanjiri, Elements Kind, hasOwnProperty vs in, va constructor function orqali prototype sharing.
24-dars
Prototype Chain
1. Muammo — nega prototype kerak?
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
arr1.push(7);
arr2.push(8);
push metodi — bu funksiya. Agar har bir array o'zining shaxsiy push nusxasini saqlasa, minglab array yaratilganda minglab bir xil funksiya nusxasi xotirada takrorlanardi. Bu behuda.
Yechim: push funksiyasi bitta joyda saqlanadi — Array.prototype da — va barcha array lar unga pointer orqali ishora qiladi. Ana shu ishora mexanizmi — Prototype Chain.
2. __proto__ — bu nima, aniq?
Har bir obyekt (deyarli har biri) ichida yashirin [[Prototype]] degan ichki slot bor (spetsifikatsiyada shunday nomlanadi). Bunga tashqi dunyodan __proto__ orqali murojaat qilinadi (yoki Object.getPrototypeOf() orqali, bu — to'g'ri, tavsiya etiladigan yo'l).
const arr = [1, 2, 3];
console.log(arr.__proto__ === Array.prototype); // true
__proto__ — bu property emas, pointer. Xuddi oldingi mavzuda ko'rganimizdek (non-primitiv larning pointer orqali ishlashi), bu yerda ham arr obyekti ichida Array.prototype obyektiga ishora qiluvchi manzil saqlanadi.
HEAP:
┌─────────────────┐
│ arr │
│ 0: 1 │
│ 1: 2 │
│ 2: 3 │
│ length: 3 │
│ [[Prototype]] ──┼──→ Array.prototype { push, pop, map, filter, ... }
└─────────────────┘ │
│ [[Prototype]]
▼
Object.prototype { toString, hasOwnProperty, ... }
│
│ [[Prototype]]
▼
null
3. Property qidirish — Prototype Chain qanday ishlaydi
arr.push(4);
V8 push ni qanday topadi?
- Avval
arrning o'z Hidden Classidapushbormi — tekshiradi. Yo'q (array o'zida faqat index lar valengthbor). arr.[[Prototype]]ga o'tadi — buArray.prototype. Undapushbormi — tekshiradi. Bor! Topildi, chaqiriladi.
Agar u yerda ham topilmaganda edi, V8 yana yuqoriga — Array.prototype.[[Prototype]] (ya'ni Object.prototype) ga o'tardi. Bu zanjir — shuning uchun "chain" deyiladi:
arr → Array.prototype → Object.prototype → null
nullga yetganda qidiruv to'xtaydi — agar hech qayerda topilmasa, natija undefined (property uchun) yoki TypeError (funksiya deb chaqirilsa, masalan arr.foo() da foo topilmasa).
4. Muhim: bu qidiruv har safar ishlaydimi?
Yo'q — aynan shu yerda oldingi mavzudagi Inline Cache (IC) yana ishga tushadi. Birinchi chaqiruvda V8 zanjir bo'ylab qidiradi va qayerda topilganini eslab qoladi (IC ga yozadi: "push — Array.prototypeda, degan offsetda"). Keyingi chaqiruvlarda, agar Hidden Class o'zgarmagan bo'lsa, V8 to'g'ridan-to'g'ri o'sha joyga boradi — qayta qidirmaydi.
Array da Hidden Class bo'ladimi?
Ha, bo'ladi — lekin arrayning ichida ikki xil narsa borligini ajratish kerak: index-based elementlar (0, 1, 2...) va named property lar (length, yoki qo'lda qo'shilgan arr.customProp). Bularning ikkalasi ham Hidden Class tizimida ishtirok etadi, lekin har xil mexanizm orqali.
1. Named property lar uchun — xuddi oddiy obyekt kabi
const arr = [1, 2, 3];
arr.label = 'mening arrayim'; // named property qo'shdik
Bu holatda V8 xuddi oddiy obyektdagidek Transition Chain orqali yangi Hidden Class yaratadi (label uchun offset qo'shiladi). Bu qism — avvalgi mavzuda ko'rganimiz bilan bir xil.
2. Index-based elementlar uchun — alohida mexanizm: Elements Kind
Bu yerda muhim farq bor. Array elementlari (0, 1, 2...) Hidden Classning "named property" qismida saqlanmaydi — ular alohida, ichki "Elements" massivida saqlanadi, va V8 bu massivning **turi (kind)**ni kuzatib boradi:
const arr1 = [1, 2, 3]; // faqat integer — PACKED_SMI_ELEMENTS
const arr2 = [1.5, 2.5]; // float — PACKED_DOUBLE_ELEMENTS
const arr3 = [1, 'two', 3]; // aralash tur — PACKED_ELEMENTS
const arr4 = [1, , 3]; // bo'sh joy (hole) bor — HOLEY_SMI_ELEMENTS
Elements Kind — bu ham Hidden Classning bir qismi, lekin maxsus: u array ichidagi qiymatlar turi va tuzilishi qandayligini bildiradi.
HiddenClass_Array: {
namedProperties: { length: offset, label: offset, ... }
elementsKind: PACKED_SMI_ELEMENTS
}
3. Elements Kind qanday "yomonlashadi" (degrade bo'ladi)
Bu — Hidden Class Transition ga juda o'xshash tushuncha, faqat array uchun. Kind lar faqat bir yo'nalishda — "tezroq"dan "sekinroq"ga qarab o'zgaradi, hech qachon orqaga qaytmaydi:
PACKED_SMI_ELEMENTS (faqat butun sonlar, bo'shliqsiz)
↓ (float qo'shilsa)
PACKED_DOUBLE_ELEMENTS
↓ (boshqa tur — string, object qo'shilsa)
PACKED_ELEMENTS
↓ (bo'sh joy — hole paydo bo'lsa, delete yoki sparse)
HOLEY_ELEMENTS ← eng sekin, eng moslashuvchan
Misol:
const arr = [1, 2, 3]; // PACKED_SMI_ELEMENTS — eng tez
arr.push(4.5); // → PACKED_DOUBLE_ELEMENTS ga o'tadi
arr.push('text'); // → PACKED_ELEMENTS ga o'tadi
delete arr[1]; // → HOLEY_ELEMENTS ga o'tadi — QAYTIB BO'LMAYDI!
Muhim: bitta delete yoki bo'sh joy ([1, , 3]) butun arrayni umrbod "holey" holatiga o'tkazadi — hatto keyin siz bo'shliqni to'ldirsangiz ham, V8 uni "ehtiyot" rejimida qoldiradi, chunki u endi har bir elementni o'qishda "bu yerda hole bormi" deb tekshirishi kerak.
4. Nega bu muhim (performance nuqtai nazaridan)
const fastArr = [1, 2, 3, 4, 5]; // PACKED_SMI — CPU cache-friendly, tezkor loop
const slowArr = [1, 2, , 4, 5]; // HOLEY_SMI — har bir elementda tekshirish kerak
PACKED_SMI_ELEMENTS massivlar ustida for loop yurishi — bu deyarli C massividagi kabi tez (ketma-ket xotira, hole tekshirish yo'q). HOLEY massivlarda esa V8 har bir elementni o'qishda qo'shimcha tekshiruv qiladi ("bu joy bo'shmi?"), bu sekinlashtiradi.
Amaliy maslahat: array yaratishda bir xil turdagi qiymatlarni ishlatish va delete/sparse array ([1, , 3]) dan qochish — yaxshi darajadagi performance odati.
Xulosa jadval bilan
| Element turi | Elements Kind | Tezlik |
|---|---|---|
[1, 2, 3] |
PACKED_SMI_ELEMENTS |
Eng tez |
[1.1, 2.2] |
PACKED_DOUBLE_ELEMENTS |
Tez |
[1, 'a', {}] |
PACKED_ELEMENTS |
O'rtacha |
[1, , 3] yoki delete arr[i] |
HOLEY_* |
Eng sekin |
5. Object.getPrototypeOf va __proto__ farqi
console.log(Object.getPrototypeOf(arr) === Array.prototype); // true
console.log(arr.__proto__ === Array.prototype); // true — bir xil natija
Ikkalasi ham bir xil narsani qaytaradi, lekin:
Object.getPrototypeOf()— standart, tavsiya etiladigan metod (ES5+).__proto__— tarixiy, ba'zi brauzerlar tomonidan qo'shilgan accessor property (Object.prototypeustida getter/setter sifatida joylashgan), keyinchalik ES2015 da rasman standartlashtirilgan, lekin hali ham "legacy" deb hisoblanadi — yangi kodda ishlatish tavsiya etilmaydi.
// __proto__ aslida Object.prototype dagi accessor:
Object.getOwnPropertyDescriptor(Object.prototype, '__proto__');
// { get: fn, set: fn, enumerable: false, configurable: true }
6. hasOwnProperty — zanjirni "kesib" tekshirish
const arr = [1, 2, 3];
console.log(arr.hasOwnProperty('length')); // true — arr'ning O'ZIDA bor
console.log(arr.hasOwnProperty('push')); // false — bu Array.prototype'da, arr'ning o'zida emas!
console.log('push' in arr); // true — 'in' butun ZANJIRNI tekshiradi
Farq juda muhim:
hasOwnProperty— faqat o'ziga tegishli (own) property larni tekshiradi, prototype chain ga chiqmaydi.inoperatori — butun zanjir bo'ylab qidiradi (prototype dagi property larni ham hisobga oladi).
7. for...in va prototype chain
Object.prototype.custom = 'zararli'; // hech qachon bunday qilmang!
const obj = { a: 1 };
for (let key in obj) {
console.log(key); // 'a', 'custom' — CHAIN dan ham chiqadi!
}
for...in — zanjir bo'ylab yuradi va enumerable bo'lgan barcha property larni (o'ziniki ham, prototype dagisini ham) ko'rsatadi. Shu sababli production kodda for...in ichida har doim hasOwnProperty bilan filtrlash tavsiya etiladi:
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
console.log(key); // faqat 'a'
}
}
(Aslida built-in metodlar — push, toString va h.k. — barchasi enumerable: false qilib yaratilgan, shuning uchun for...inda odatda ko'rinmaydi. Yuqoridagi misolda qo'lda enumerable: true bo'lgan custom qo'shildi — shuning uchun muammo ko'rindi.)
8. Constructor function orqali prototype qanday belgilanadi
function Person(name) {
this.name = name;
}
Person.prototype.greet = function() {
return `Salom, men ${this.name}`;
};
const p1 = new Person('Ali');
const p2 = new Person('Vali');
console.log(p1.__proto__ === Person.prototype); // true
console.log(p2.__proto__ === Person.prototype); // true — IKKALASI HAM bitta prototype'ga ishora qiladi!
p1.greet(); // 'Salom, men Ali' — greet Person.prototype'dan topiladi
p1 va p2 — ikkalasi ham bitta Person.prototype obyektiga ishora qiladi (nusxalanmagan!). greet funksiyasi xotirada faqat bir marta yaratiladi, minglab Person instance yaratilsa ham.
HEAP:
p1 { name: 'Ali', [[Prototype]] ──┐
p2 { name: 'Vali', [[Prototype]] ──┼──→ Person.prototype { greet: fn }
│ │
│ │ [[Prototype]]
│ ▼
│ Object.prototype
Bu — xotira tejash va metodlarni bo'lishish (sharing) mexanizmi, prototype-based OOP ning aynan shu narsa uchun ixtiro qilingan asosiy sababi.
9. Prototype ustiga yozish (shadowing) — muhim nozik nuqta
function Person(name) {
this.name = name;
}
Person.prototype.greet = function() {
return 'umumiy salom';
};
const p1 = new Person('Ali');
p1.greet = function() {
return 'shaxsiy salom';
}; // p1'ning O'ZIGA yangi property qo'shildi
console.log(p1.greet()); // 'shaxsiy salom' — o'zidagi topildi, prototype ga bormaydi ham!
console.log(Person.prototype.greet === p1.greet); // false — ikki xil funksiya endi
Bu shadowing deyiladi — p1ning o'zida greet paydo bo'lgach, qidiruv birinchi bosqichda to'xtaydi, prototype dagi greetga hech qachon yetib bormaydi. Prototype dagi asl greet — o'zgarmagan, faqat p1 uni "yashirgan".
10. Xulosa — vizual umumlashtirish
new Person('Ali') → p1 { name: 'Ali' }
│
│ [[Prototype]] (pointer, nusxa emas!)
▼
Person.prototype { greet }
│
│ [[Prototype]]
▼
Object.prototype { toString, hasOwnProperty, ... }
│
│ [[Prototype]]
▼
null ← zanjir shu yerda tugaydi