Seriya: JavaScript Basics Dars 23

Property Descriptor

Property lar orqasidagi yashirin metadata — writable, enumerable, configurable, accessor descriptor, freeze/seal va extensible.

23-dars

Property Descriptor

1. Descriptor nima o'zi?

const user = { name: 'Ali' };

Siz user.name ni oddiy "qiymat" deb o'ylaysiz. Lekin JS spetsifikatsiyasi (ECMA-262) da har bir property aslida yashirin metadata bilan birga saqlanadi. Bu metadata Property Descriptor deyiladi.

Ko'rish uchun:

const user = { name: 'Ali' };
console.log(Object.getOwnPropertyDescriptor(user, 'name'));

// {
//   value: 'Ali',
//   writable: true,
//   enumerable: true,
//   configurable: true
// }

Demak, name: 'Ali' yozganingizda, JS engine sizga ko'rinmagan holda 4 ta flag qo'shadi. Bular — property ning "huquqlari".


2. Ikki xil Descriptor turi bor

JS da property ikki xil bo'lishi mumkin:

A) Data Descriptor — oddiy qiymat saqlaydi:

{ value, writable, enumerable, configurable }

B) Accessor Descriptor — getter/setter orqali ishlaydi:

{ get, set, enumerable, configurable }

Muhim qoida: bitta property ikkalasi ham bo'la olmaydi. Yoki value bor, yoki get/set bor — ikkalasi birga bo'lsa, TypeError chiqadi.

Object.defineProperty(obj, 'x', {
  value: 10,
  get() { return 20; } // ❌ TypeError: Invalid property descriptor
});

3. Har bir flag nima qiladi

writable

Property ning qiymatini o'zgartirish mumkinmi, degan savol.

const obj = {};
Object.defineProperty(obj, 'x', {
  value: 10,
  writable: false,
  enumerable: true,
  configurable: true
});

obj.x = 99; // sokin ignore qilinadi (strict mode da TypeError)
console.log(obj.x); // 10

Strict modeda ('use strict' yoki module/class ichida — ular default strict) bu operatsiya xato tashlaydi:

'use strict';
obj.x = 99; // TypeError: Cannot assign to read only property 'x'

enumerable

Property for...in, Object.keys(), JSON.stringify() kabi iteratsiyalarda ko'rinadimi, degan savol.

const obj = {};
Object.defineProperty(obj, 'hidden', {
  value: 'sir',
  enumerable: false,
  writable: true,
  configurable: true
});

console.log(Object.keys(obj));        // []
console.log(JSON.stringify(obj));     // {}
console.log(obj.hidden);              // 'sir' — to'g'ridan-to'g'ri o'qish ISHLAYDI!

Muhim nozik farq: enumerable: false faqat ko'rinishni yashiradi (iteration dan), lekin propertyga to'g'ridan-to'g'ri murojaat qilish hali ham ishlaydi. Bu "private" degani emas — bu faqat "ro'yxatda ko'rinmasin" degani.

Aynan shu mexanizm orqali built-in metodlar yashiringan:

console.log(Object.keys([1,2,3])); // ['0', '1', '2'] — 'length' ko'rinmaydi

length — enumerable: false, shuning uchun Object.keys uni ko'rsatmaydi, garchi u haqiqatan bor bo'lsa ham.

configurable

Bu eng "kuchli" flag. Ikki narsani nazorat qiladi:

  1. Property ni delete qilish mumkinmi
  2. Property ning descriptorini (flag larni) qayta o'zgartirish mumkinmi
const obj = {};
Object.defineProperty(obj, 'x', {
  value: 10,
  configurable: false,
  writable: true,
  enumerable: true
});

delete obj.x; // false qaytadi, obj.x hali ham 10 (strict modeda TypeError)

Object.defineProperty(obj, 'x', { enumerable: false });
// ❌ TypeError: Cannot redefine property: x

Nozik istisno: configurable: false bo'lsa ham, agar writable: true bo'lsa, siz hali ham qiymatni obj.x = 20 orqali o'zgartira olasiz (chunki bu descriptor'ni qayta belgilash emas, oddiy yozish). Lekin writableni defineProperty orqali qayta o'zgartirmoqchi bo'lsangiz — bu bir tomonlama yo'l: writable: true → false ga o'tkazish mumkin, lekin writable: false → true ga qaytarish, configurable: false bo'lsa, mumkin emas.

Object.defineProperty(obj, 'y', {
  value: 5, writable: true, configurable: false, enumerable: true
});

obj.y = 10;          // ✅ ishlaydi — writable true
console.log(obj.y);  // 10

Object.defineProperty(obj, 'y', { writable: false }); // ✅ ruxsat — true→false mumkin
Object.defineProperty(obj, 'y', { writable: true });  // ❌ TypeError — false→true configurable:false da mumkin emas

4. Oddiy obj.x = 10 bilan Object.defineProperty farqi

const obj = {};
obj.x = 10;

Bu yozganda JS engine avtomatik default descriptor qo'yadi:

{ value: 10, writable: true, enumerable: true, configurable: true }

Ya'ni — oddiy property yozish = "hammasiga ruxsat" bilan yaratish.

Object.defineProperty(obj, 'x', { value: 10 });

Bu esa default qiymatlar falsedan boshlanadi:

{ value: 10, writable: false, enumerable: false, configurable: false }

Bu eng ko'p uchraydigan xato manbasi: dasturchilar defineProperty ishlatib, keyin nega property for...in da ko'rinmayapti yoki nega o'zgarmayapti deb hayron bo'lishadi — sababi, ular writable/enumerableni aniq true qilib yozishni unutishadi.


5. Accessor Descriptor — getter/setter

const user = {
  firstName: 'Ali',
  lastName: 'Vali',
  get fullName() {
    return `${this.firstName} ${this.lastName}`;
  },
  set fullName(value) {
    [this.firstName, this.lastName] = value.split(' ');
  }
};

console.log(user.fullName);   // 'Ali Vali' — get chaqiriladi
user.fullName = 'Hasan Ahmad';
console.log(user.firstName);  // 'Hasan'

V8 ichida bu qanday saqlanadi? fullName uchun Hidden Class da value yo'q, o'rniga get/set funksiyalariga pointer bor:

HiddenClass: {
  fullName → AccessorDescriptor { getter: fn, setter: fn }
}

Har safar user.fullName o'qilganda, V8 to'g'ridan-to'g'ri qiymatni offsetdan olmaydi — u getter funksiyasini chaqiradi. Shuning uchun accessor property lar data property larga qaraganda sekinroq — chunki bu aslida funksiya chaqiruvi, oddiy memory access emas.


6. Object.freeze va Object.seal — descriptor larning amaliy qo'llanilishi

const obj = { x: 1, y: 2 };
Object.freeze(obj);

Object.freeze — barcha property larni writable: false, configurable: false qiladi (obyektning o'ziga yangi property qo'shish ham taqiqlanadi — extensible: false).

obj.x = 100;      // sokin ignore (strict modeda TypeError)
obj.z = 5;        // yangi property qo'sha olmaysiz
delete obj.x;     // o'chira olmaysiz
console.log(obj); // { x: 1, y: 2 } — o'zgarmadi

Object.seal — faqat configurable: false qiladi (delete va yangi property qo'shish taqiqlanadi), lekin writable qoladi — ya'ni mavjud property larning qiymatini o'zgartirish mumkin:

const obj2 = { x: 1 };
Object.seal(obj2);
obj2.x = 99;   // ✅ ishlaydi — writable hali true
obj2.y = 5;    // ❌ yangi qo'sha olmaysiz
delete obj2.x; // ❌ o'chira olmaysiz

Farq bir gapda: freeze = "hech narsa o'zgarmasin" (qattiq immutable), seal = "struktura o'zgarmasin, lekin qiymatlar o'zgarishi mumkin".

⚠️ Muhim: ikkalasi ham shallow — faqat birinchi darajadagi property larga ta'sir qiladi:

const obj = Object.freeze({ nested: { a: 1 } });
obj.nested.a = 999; // ✅ ISHLAYDI! nested obyekt freeze qilinmagan

7. extensible nima?

Bu — biz ko'rgan writable, enumerable, configurable kabi property darajasidagi flag emas, balki butun obyektning o'ziga tegishli flag. Ya'ni u aynan bitta property haqida emas — "bu obyektga umuman yangi property qo'shsa bo'ladimi?" degan savolga javob beradi.

Har bir obyekt default holatda — extensible: true

const obj = { x: 1 };
console.log(Object.isExtensible(obj)); // true

obj.y = 2; // ✅ yangi property qo'shildi — muammosiz
console.log(obj); // { x: 1, y: 2 }

Oddiy holatda har qanday obyektga xohlagancha yangi property qo'shish mumkin — chunki u "extensible" (kengaytirilishi mumkin).

Object.preventExtensions() — kengaytirishni to'xtatish

const obj = { x: 1 };
Object.preventExtensions(obj);

console.log(Object.isExtensible(obj)); // false

obj.y = 2; // sokin ignore qilinadi (strict modeda TypeError)
console.log(obj); // { x: 1 } — 'y' qo'shilmadi!

Muhim: bu faqat yangi property qo'shishni taqiqlaydi. Mavjud property lar bilan hech narsa o'zgarmaydi:

obj.x = 100;  // ✅ ishlaydi — x mavjud property, uni o'zgartirish mumkin (writable true bo'lsa)
delete obj.x; // ✅ ishlaydi — x ni o'chirish mumkin (configurable true bo'lsa)

Ya'ni preventExtensions — faqat "structure" ustidan nazorat: "yangi kirish taqiqlangan, lekin mavjudlar o'z holicha erkin".

Uch darajali "qattiqlik" tushunchasi

Endi to'liq tasavvur qilish uchun uchta metodni solishtiramiz — ular bir-biridan kuchliroq bo'lib boradi:

Metod Yangi property qo'shish Mavjud property o'chirish Mavjud property qiymatini o'zgartirish
Object.preventExtensions() ❌ ✅ mumkin ✅ mumkin
Object.seal() ❌ ❌ ✅ mumkin
Object.freeze() ❌ ❌ ❌

Object.seal() ichida aslida shu bo'ladi:

Object.preventExtensions(obj);       // 1. kengaytirish taqiqlanadi
// + barcha mavjud property larga configurable: false qo'yiladi

Object.freeze() ichida esa:

Object.preventExtensions(obj);       // 1. kengaytirish taqiqlanadi
// + configurable: false          (seal kabi)
// + writable: false               (bundan tashqari, qiymat ham qotib qoladi)

Ya'ni bu uchtasi — bir-birining ustiga qurilgan, bosqichma-bosqich kuchayib boruvchi cheklovlar:

preventExtensions  →  seal  →  freeze
(faqat yangi qo'shish taqiqlanadi)  →  (+ o'chirish taqiqlanadi)  →  (+ o'zgartirish ham taqiqlanadi)

8. V8 ichida nima bo'ladi?

Eslaysizmi, har bir obyekt Hidden Classga ega va yangi property qo'shilganda Transition Chain orqali yangi Hidden Classga o'tadi edi.

const obj = { x: 1 }; // HiddenClass: C1 {x}
Object.preventExtensions(obj);
obj.y = 2; // Transition qila olmaydi — chunki extensible: false

extensible: false qilingach, V8 uchun bu obyekt endi hech qachon yangi Hidden Class'ga o'tolmaydi — Transition Chain shu yerda "yopiladi". Bu ma'lum darajada V8'ga optimallashtirish imkoniyati ham beradi, chunki obyekt shakli endi hech qachon o'zgarmasligi kafolatlanadi.


9. Tekshirish uchun 3 ta metod

const obj = { x: 1 };

Object.isExtensible(obj);  // true/false — kengaytirilishi mumkinmi?
Object.isSealed(obj);      // true/false — seal qilinganmi? (extensible:false + barcha configurable:false)
Object.isFrozen(obj);      // true/false — freeze qilinganmi? (seal + barcha writable:false)

Nozik nuqta: bo'sh obyektda ({}) Object.isSealed({}) va Object.isFrozen({}) — ikkalasi ham true qaytaradi, garchi hech narsa qilinmagan bo'lsa ham! Sababi — sealed/frozen bo'lish shartlari (hech qanday configurable/writable property yo'q) bo'sh obyekt uchun avtomatik bajariladi:

console.log(Object.isSealed({}));  // true
console.log(Object.isFrozen({}));  // true