JavaScript المتقدم (ES6+)

الـ Getters والـ Setters والـ Descriptors

13 دقيقة الدرس 31 من 40

الـ Getters والـ Setters والـ Descriptors

في هذا الدرس، سنستكشف آليات الوصول القوية للخصائص في JavaScript: الـ getters والـ setters والـ property descriptors. تمنحك هذه الميزات تحكماً دقيقاً في كيفية تصرف الخصائص في كائناتك.

فهم الـ Getters والـ Setters

تسمح لك الـ getters والـ setters بتعريف دوال تُنفذ عند الوصول إلى خاصية أو تعديلها، مما يمنحك التحكم في الوصول إلى الخصائص:

Getter و Setter أساسيان: const user = { firstName: 'John', lastName: 'Doe', get fullName() { return `${this.firstName} ${this.lastName}`; }, set fullName(value) { const parts = value.split(' '); this.firstName = parts[0]; this.lastName = parts[1]; } }; console.log(user.fullName); // John Doe user.fullName = 'Jane Smith'; console.log(user.firstName); // Jane console.log(user.lastName); // Smith
مفهوم أساسي: تبدو الـ getters والـ setters مثل الخصائص العادية لكنها تنفذ دوال خلف الكواليس. يسمح لك هذا بحساب القيم أو التحقق من المدخلات أو تشغيل آثار جانبية.

الـ Getters والـ Setters في الفئات (Classes)

تستخدم الفئات الـ getters والـ setters على نطاق واسع للتغليف:

class Temperature { constructor(celsius) { this._celsius = celsius; } get celsius() { return this._celsius; } set celsius(value) { if (value < -273.15) { throw new Error('Temperature below absolute zero!'); } this._celsius = value; } get fahrenheit() { return this._celsius * 9/5 + 32; } set fahrenheit(value) { this.celsius = (value - 32) * 5/9; } } const temp = new Temperature(25); console.log(temp.celsius); // 25 console.log(temp.fahrenheit); // 77 temp.fahrenheit = 86; console.log(temp.celsius); // 30

واصفات الخصائص (Property Descriptors)

كل خاصية في JavaScript لها واصف خاصية يحدد سلوكها. يمكنك عرض هذه الواصفات وتعديلها:

الحصول على واصفات الخصائص: const obj = { name: 'John' }; const descriptor = Object.getOwnPropertyDescriptor(obj, 'name'); console.log(descriptor); // { // value: 'John', // writable: true, // enumerable: true, // configurable: true // }

سمات واصفات الخصائص

فهم السمات الأربع الرئيسية لواصفات الخصائص:

سمات الواصف: 1. value: قيمة الخاصية 2. writable: هل يمكن تغيير القيمة؟ 3. enumerable: هل ستظهر في حلقات for...in؟ 4. configurable: هل يمكن تغيير الواصف أو حذف الخاصية؟
نصيحة: بالنسبة للخصائص الوصولية (getters/setters)، يحتوي الواصف على سمات 'get' و 'set' بدلاً من 'value' و 'writable'.

تعريف الخصائص باستخدام الواصفات

استخدم Object.defineProperty() لإنشاء خصائص بواصفات محددة:

const person = {}; Object.defineProperty(person, 'age', { value: 30, writable: false, // للقراءة فقط enumerable: true, configurable: false }); person.age = 40; // يفشل بصمت (الوضع الصارم: يطرح خطأ) console.log(person.age); // 30 delete person.age; // يفشل لأن configurable خطأ console.log(person.age); // 30

إنشاء خصائص غير قابلة للعد (Non-Enumerable)

الخصائص غير القابلة للعد لا تظهر في الحلقات أو Object.keys():

const user = { name: 'John', email: 'john@example.com' }; Object.defineProperty(user, 'password', { value: 'secret123', enumerable: false, // مخفي من العد writable: true, configurable: true }); console.log(user.password); // secret123 console.log(Object.keys(user)); // ['name', 'email'] console.log(JSON.stringify(user)); // {"name":"John","email":"john@example.com"}

الخصائص المحسوبة باستخدام Getters

استخدم الـ getters لإنشاء خصائص محسوبة تُشتق من خصائص أخرى:

class Circle { constructor(radius) { this.radius = radius; } get diameter() { return this.radius * 2; } get circumference() { return 2 * Math.PI * this.radius; } get area() { return Math.PI * this.radius ** 2; } } const circle = new Circle(5); console.log(circle.diameter); // 10 console.log(circle.circumference); // 31.41592653589793 console.log(circle.area); // 78.53981633974483

التحقق من الصحة باستخدام Setters

الـ setters مثالية للتحقق من صحة المدخلات قبل تخزين القيم:

class User { constructor(name, age) { this.name = name; this.age = age; } set age(value) { if (typeof value !== 'number') { throw new TypeError('Age must be a number'); } if (value < 0 || value > 150) { throw new RangeError('Age must be between 0 and 150'); } this._age = value; } get age() { return this._age; } } const user = new User('John', 30); console.log(user.age); // 30 // user.age = 'thirty'; // TypeError: Age must be a number // user.age = 200; // RangeError: Age must be between 0 and 150
مهم: عند استخدام setters للتحقق من الصحة، استخدم اسم خاصية داخلية مختلفاً (مثل _age) لتجنب التكرار اللانهائي.

نمط الخصائص الخاصة

استخدم closures أو WeakMaps مع getters/setters لإنشاء خصائص خاصة حقيقية:

استخدام WeakMap للخصوصية: const privateData = new WeakMap(); class BankAccount { constructor(balance) { privateData.set(this, { balance }); } get balance() { return privateData.get(this).balance; } deposit(amount) { if (amount <= 0) { throw new Error('Amount must be positive'); } const data = privateData.get(this); data.balance += amount; } withdraw(amount) { const data = privateData.get(this); if (amount > data.balance) { throw new Error('Insufficient funds'); } data.balance -= amount; } } const account = new BankAccount(1000); console.log(account.balance); // 1000 account.deposit(500); console.log(account.balance); // 1500 // لا توجد طريقة للوصول أو تعديل الرصيد مباشرة

تعريف خصائص متعددة

استخدم Object.defineProperties() لتعريف خصائص متعددة في آن واحد:

const product = {}; Object.defineProperties(product, { name: { value: 'Laptop', writable: true, enumerable: true, configurable: true }, price: { value: 999.99, writable: false, enumerable: true, configurable: false }, inStock: { value: true, writable: true, enumerable: false, configurable: true } }); console.log(product.name); // Laptop console.log(product.price); // 999.99 console.log(Object.keys(product)); // ['name', 'price']

واصفات الوصول باستخدام defineProperty

أنشئ getters و setters باستخدام Object.defineProperty():

const rectangle = { _width: 10, _height: 5 }; Object.defineProperty(rectangle, 'area', { get() { return this._width * this._height; }, enumerable: true, configurable: true }); Object.defineProperty(rectangle, 'width', { get() { return this._width; }, set(value) { if (value <= 0) { throw new Error('Width must be positive'); } this._width = value; }, enumerable: true, configurable: true }); console.log(rectangle.area); // 50 rectangle.width = 20; console.log(rectangle.area); // 100

مثال من العالم الحقيقي: مدقق النماذج

إليك مثالاً عملياً يستخدم getters و setters والتحقق من الصحة:

class FormData { constructor() { this._errors = {}; } set email(value) { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(value)) { this._errors.email = 'Invalid email format'; } else { delete this._errors.email; this._email = value; } } get email() { return this._email; } set password(value) { if (value.length < 8) { this._errors.password = 'Password must be at least 8 characters'; } else { delete this._errors.password; this._password = value; } } get password() { return this._password; } get isValid() { return Object.keys(this._errors).length === 0; } get errors() { return { ...this._errors }; } } const form = new FormData(); form.email = 'invalid-email'; form.password = 'short'; console.log(form.isValid); // false console.log(form.errors); // { email: 'Invalid email format', password: 'Password must be at least 8 characters' } form.email = 'user@example.com'; form.password = 'secure-password-123'; console.log(form.isValid); // true

تمرين تطبيقي:

المهمة: أنشئ فئة Product بالمتطلبات التالية:

  1. خصائص خاصة لـ price و discount
  2. Getter لـ finalPrice يحسب السعر بعد الخصم
  3. Setter لـ discount يسمح فقط بقيم بين 0 و 100
  4. خاصية غير قابلة للعد id

الحل:

const privateProps = new WeakMap(); class Product { constructor(name, price, id) { this.name = name; privateProps.set(this, { price, discount: 0 }); Object.defineProperty(this, 'id', { value: id, writable: false, enumerable: false, configurable: false }); } get price() { return privateProps.get(this).price; } set price(value) { if (value < 0) { throw new Error('Price cannot be negative'); } privateProps.get(this).price = value; } get discount() { return privateProps.get(this).discount; } set discount(value) { if (value < 0 || value > 100) { throw new RangeError('Discount must be between 0 and 100'); } privateProps.get(this).discount = value; } get finalPrice() { const { price, discount } = privateProps.get(this); return price * (1 - discount / 100); } } const laptop = new Product('Laptop', 1000, 'PROD-001'); console.log(laptop.price); // 1000 laptop.discount = 20; console.log(laptop.finalPrice); // 800 console.log(Object.keys(laptop)); // ['name'] (id غير قابل للعد)

الملخص

في هذا الدرس، تعلمت:

  • كيفية استخدام getters و setters للخصائص المحسوبة والمتحقق منها
  • واصفات الخصائص تتحكم في سمات writable و enumerable و configurable
  • Object.defineProperty() و Object.defineProperties() للتحكم الدقيق
  • إنشاء خصائص غير قابلة للعد وللقراءة فقط
  • استخدام getters/setters للتحقق من الصحة والتغليف
  • أنماط لإنشاء خصائص خاصة باستخدام WeakMap
التالي: في الدرس التالي، سنستكشف وحدات ES6 وأنظمة الوحدات الحديثة في JavaScript!