EduFlow
أساسيات JavaScript
⁦0%⁩

الوحدة 5 · كتابة برامج حقيقية

الأصناف (Classes)

16 دقراءة 3 دبيئة تجريب الكود

الدرس في لمحة

تعرّف الأصناف (classes) بـ constructor وmethods وgetters وحقول خاصة، وتُنشئ instances مستقلة بـ new، وتتجنّب خطأ ضياع this الكلاسيكي حين تمرّر الـ methods على أنها callbacks.

  1. 1تعريف صنف
  2. 2الـ Instances: مخطّط واحد وكائنات كثيرة
  3. 3ضياع this

لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.

انظر إلى ما يفعله كود Pocket بمصاريفه: متغيّر يحمل المصفوفة، ونحو اثنتي عشرة دالة تأخذ كل منها تلك المصفوفة، وتفعل شيئًا، وتُرجع نتيجة. البيانات والعمليات عليها منفصلة، فلا شيء يمنع سطر كود شاردًا من دفع مصروف مشوّه مباشرة إلى المصفوفة. أما الصنف (class) فيحزم البيانات مع الدوال المسموح لها بالعمل عليها، ويستطيع أن يجعل البيانات نفسها بعيدة عن متناول الخارج.

تعريف صنف

JavaScript
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 فتُعرَّف مرة واحدة على الصنف وتُتشارك.

يعرّف الصنف ExpenseStore الـ methods‏ add وremove وlist والـ getter‏ total. ولكل من الـ instances‏ home وwork مصفوفة expenses خاصة بها، وكلتاهما تستخدمان الـ methods المشتركة. class ExpenseStore add, remove, list get total home #expenses: 12 عنصرًا work #expenses: 3 عناصر new ExpenseStore()
صنف واحد وinstances كثيرة لكل منها بياناتها
JavaScript
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 إلى مكان ما وحدها، فلن تكون هناك نقطة حين تُستدعى في النهاية:

JavaScript
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).

قراءات إضافية

تحقّق من فهمك

3 أسئلة

السؤال 1 من 3

ماذا يطبع هذا الكود؟

JavaScript
class Counter {
  count = 0;
  increment() { this.count++; }
}
const a = new Counter();
const b = new Counter();
a.increment();
a.increment();
console.log(a.count, b.count);
السؤال 2 من 3

button.addEventListener("click", store.clear); تجعل clear ترمي TypeError عن الحقل الخاص #expenses (“…from an object whose class did not declare it”) حين يُنقر الزر. لماذا؟

السؤال 3 من 3

يحتفظ ExpenseStore بقائمته في #expenses. ماذا تفعل store.#expenses المكتوبة خارج الصنف؟

تدريب

بيئة تجريب الكود

مخزن يحمي بياناته

أعِد كتابة ExpenseStore بحيث لا تتغيّر بياناته إلا عبر الـ methods الخاصة به:

  • احتفظ بالمصاريف في حقل خاص #expenses، كي لا تكون للـ instances أي خصائص عامة.
  • add({ label, amount, category }) تُنشئ مصروفًا له معرّف ("exp-1" و"exp-2"… يُعدّ لكل مخزن على حدة)، وcategory قيمتها الافتراضية "other"، وتخزّنه وتُرجعه.
  • remove(id) تحذف المصروف صاحب ذلك المعرّف.
  • list() تُرجع نسخة من مصفوفة المصاريف.
  • total هو getter يُرجع مجموع كل المبالغ.
  • totalFor(category) تُرجع المجموع لفئة واحدة (0 إن لم توجد).
// Pocket's data, unprotected: any code can push anything into it.
class ExpenseStore {
  constructor() {
    this.expenses = [];
  }

  add(expense) {
    this.expenses.push(expense);
  }
}

const store = new ExpenseStore();
store.add({ label: "Coffee", amount: 450, category: "food" });
console.log(store.expenses);
جارٍ تحميل المحرّر…

Ctrl/⌘ + Enter للتشغيل · يعمل في بيئة معزولة داخل متصفّحك. لا شيء يغادر جهازك.

الـ console

(لا مخرجات)

الفحوص

  • add تُرجع المصروف الجديد مع معرّف وفئة افتراضية
  • list تُرجع نسخة، فتغييرها لا يغيّر المخزن
  • ليست للمخزن أي خصائص بيانات عامة
  • remove تحذف المصروف صاحب ذلك المعرّف
  • total هو getter يبقى محدّثًا
  • totalFor تجمع فئة واحدة
  • مخزنان يحتفظ كلٌّ منهما ببياناته وعدّاد معرّفاته المنفصل