الـ 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 بالمتطلبات التالية:
- خصائص خاصة لـ
price و discount
- Getter لـ
finalPrice يحسب السعر بعد الخصم
- Setter لـ
discount يسمح فقط بقيم بين 0 و 100
- خاصية غير قابلة للعد
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!