Series: JavaScript Basics Lesson 24

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?

  1. Avval arrning o'z Hidden Classida push bormi — tekshiradi. Yo'q (array o'zida faqat index lar va length bor).
  2. arr.[[Prototype]] ga o'tadi — bu Array.prototype. Unda push bormi — 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.prototype ustida 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.
  • in operatori — 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