الوحدة 5 · كتابة برامج حقيقية
الأصناف (Classes)
16 دقراءة 3 دبيئة تجريب الكود
الدرس في لمحة
تعرّف الأصناف (classes) بـ constructor وmethods وgetters وحقول خاصة، وتُنشئ instances مستقلة بـ new، وتتجنّب خطأ ضياع this الكلاسيكي حين تمرّر الـ methods على أنها callbacks.
لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.
انظر إلى ما يفعله كود Pocket بمصاريفه: متغيّر يحمل المصفوفة، ونحو اثنتي عشرة دالة تأخذ كل منها تلك المصفوفة، وتفعل شيئًا، وتُرجع نتيجة. البيانات والعمليات عليها منفصلة، فلا شيء يمنع سطر كود شاردًا من دفع مصروف مشوّه مباشرة إلى المصفوفة. أما الصنف (class) فيحزم البيانات مع الدوال المسموح لها بالعمل عليها، ويستطيع أن يجعل البيانات نفسها بعيدة عن متناول الخارج.
تعريف صنف
class ExpenseStore {
#expenses = [];
#nextNumber = 1;
constructor(initial = []) {
for (const expense of initial) {
this.add(expense);
}
}
add({ label, amount, category = "other" }) {
const expense = { id: `exp-${this.#nextNumber++}`, label, amount, category };
this.#expenses.push(expense);
return expense;
}
remove(id) {
this.#expenses = this.#expenses.filter((e) => e.id !== id);
}
get total() {
return this.#expenses.reduce((sum, e) => sum + e.amount, 0);
}
list() {
return [...this.#expenses];
}
}
const store = new ExpenseStore([{ label: "Coffee", amount: 450, category: "food" }]);
store.add({ label: "Train", amount: 1220, category: "travel" });
console.log(store.total, store.list().map((e) => e.id));هناك الكثير هنا، فخذه قطعة قطعة.
#expenses = [] تصرّح بـ حقل خاص (private field). كل instance (نسخة) تحصل على #expenses خاص بها، وعلامة # تجعله خاصًا: لا يستطيع قراءته أو الكتابة فيه إلا الكود الموجود داخل جسم الصنف. وفي الخارج، store.#expenses خطأ في الصياغة. ومن يريد تغيير القائمة عليه أن يمرّ عبر add وremove، اللتين تضمنان أن يحصل كل مصروف على معرّف وفئة.
الـ constructor (الباني) method خاصة تُنفَّذ حين تكتب new ExpenseStore(...). ومهمتها تهيئة الكائن الجديد. وهي هنا تضيف أي مصاريف ابتدائية عبر add، فتخضع للقواعد نفسها.
add وremove وlist هي methods (دوال الكائن). وداخل الـ method، تشير this إلى الكائن الذي استُدعيت عليه: في store.add(...)، تكون this هي store.
get total() هو getter. تقرؤه كأنه خاصية، store.total، دون أقواس، لكنه ينفّذ كودًا في كل مرة. فالمجموع المخزّن في حقل قد يصبح قديمًا حين يُحذف مصروف؛ أما الـ getter فيُحسب دائمًا من البيانات الحالية.
list() تُرجع نسخة، [...this.#expenses]، لا المصفوفة نفسها. فإرجاع المصفوفة الحقيقية سيسمح للمُستدعين بـ push فيها، ويُبطل كل الحماية.
الـ Instances: مخطّط واحد وكائنات كثيرة
new تُنشئ كائنًا جديدًا، وتنفّذ الـ constructor وthis تشير إليه، ثم تُرجعه. لكل instance حقولها الخاصة؛ أما الـ methods فتُعرَّف مرة واحدة على الصنف وتُتشارك.
class ExpenseStore {
#expenses = [];
add(expense) { this.#expenses.push(expense); }
get count() { return this.#expenses.length; }
}
const home = new ExpenseStore();
const work = new ExpenseStore();
home.add({ label: "Groceries", amount: 6400 });
console.log(home.count, work.count);إن ذكّرك هذا بـ createBudget من درس الـ closures، فهذا طبيعي. الـ closure والصنف ذو الحقول الخاصة يحلّان المشكلة نفسها: بيانات لا تستطيع لمسها إلا دوال معيّنة. الـ closures أخفّ مع دالة واحدة؛ والأصناف أوضح حين تتشارك عدّة عمليات البيانات نفسها.
ضياع this
this لا تُثبَّت حين تكتب الـ method. بل تتحدّد في كل مرة تُستدعى فيها الدالة، بحسب ما يقع على يسار النقطة. store.add(x) تستدعي add وthis مضبوطة على store. لكن إن مرّرت الـ method إلى مكان ما وحدها، فلن تكون هناك نقطة حين تُستدعى في النهاية:
class Greeter {
name = "Pocket";
hello() {
return `Hello from ${this.name}`;
}
}
const g = new Greeter();
console.log(g.hello());
const detached = g.hello;
try {
console.log(detached());
} catch (error) {
console.log("Error:", error.message);
}
const wrapped = () => g.hello();
console.log(wrapped());الاستدعاء الأول ينجح لأن g على يسار النقطة. أما detached() فتُستدعى ولا شيء على يسار أي نقطة، فتكون this داخلها undefined (أجسام الأصناف تعمل دائمًا في الوضع الصارم)، وقراءة this.name ترمي خطأ.
يمكن للأصناف أيضًا أن تُبنى فوق أصناف أخرى باستخدام extends، فترث methods الصنف الآخر وتضيف methods جديدة. ستستخدم ذلك مرة واحدة، في الدرس التالي، لتصنع نوع خطأ خاصًا بك. وفيما عدا ذلك، فالخيار الافتراضي الجيد في كود التطبيقات هو تفضيل الأصناف الصغيرة والدوال العادية على سلاسل الوراثة العميقة، التي يصعب تغييرها عادةً.
صار مخزنك يرفض التغييرات السيئة من الخارج. والدرس التالي عمّا يحدث حين يسوء شيء ما رغم ذلك: الأخطاء، وكيف ترميها وتلتقطها عن قصد، وكيف تطارد الأخطاء البرمجية بأدوات DevTools في المتصفّح.
أهم النقاط
- الصنف (class) مخطّط؛ و
new ClassName(...)تنفّذ الـ constructor وتُرجع instance جديدة لها بياناتها الخاصة. - الـ methods مشتركة بين كل الـ instances، وداخلها تشير
thisإلى الـ instance التي استُدعيت عليها الـ method. - الحقول الخاصة (
#expenses) لا يمكن قراءتها إلا داخل جسم الصنف، وهذا يحمي بيانات الكائن كما تفعل الـ closures. - الـ getter (
get total()) يُقرأ كأنه خاصية لكنه يُحسب في كل مرة، فلا تصبح القيم المشتقّة قديمة أبدًا. - تمرير
store.addعلى أنها callback يُضيّعthis؛ غلّفها بدالة سهمية:(e) => store.add(e).