تخطَّ إلى المحتوى

الكلاسات في JavaScript: class والوراثة

الدرس 26 من 28 · JavaScript

تعلمت أن تنشئ كائناً بكتابة خصائصه بين قوسين معقوفين. لكن ماذا لو احتاج برنامجك مئة طالب، لكل واحد الخصائص نفسها والدوال نفسها؟ لن تكتب الكائن مئة مرة. في هذا الدرس ستتعلم الكلاس (Class): قالب تكتبه مرة واحدة، ثم تصنع منه ما تشاء من الكائنات.

الكلاس وصف لنوع من الكائنات: ما الخصائص التي يحملها، وما الذي يستطيع فعله.

تخيّل قالب الكعك: القالب نفسه لا يؤكل، لكنك تصنع به عشرات الكعكات، لها الشكل نفسه، ولكل واحدة حشوتها. القالب هو الكلاس، وكل كعكة كائن صُنع منه.

class Student {
constructor(name, grade) {
this.name = name;
this.grade = grade;
}
describe() {
return `${this.name} في الصف ${this.grade}`;
}
}
const sara = new Student("سارة", 9);
const omar = new Student("عمر", 10);
console.log(sara.describe());
console.log(omar.describe());
console.log(sara.name);
الناتج
سارة في الصف 9
عمر في الصف 10
سارة
  • class Student: اسم الكلاس، ويبدأ بحرف كبير في العُرف المتبع.
  • constructor: دالة خاصة تُنفَّذ تلقائياً عند إنشاء كل كائن، ووظيفتها تجهيز خصائصه.
  • this: تشير إلى الكائن الذي يُنشأ الآن. فالسطر this.name = name يضع الاسم في هذا الكائن بالذات.
  • describe(): دالة يملكها كل كائن من الكلاس. لاحظ أنها تُكتب بدون كلمة function، وبدون فواصل بين الدوال.
  • new Student(...): تصنع كائناً جديداً وتمرر القيم إلى constructor.
class Wallet {
constructor(owner) {
this.owner = owner;
this.balance = 0;
}
deposit(amount) {
if (amount <= 0) {
throw new Error("المبلغ يجب أن يكون أكبر من صفر");
}
this.balance += amount;
}
withdraw(amount) {
if (amount > this.balance) {
throw new Error("الرصيد لا يكفي");
}
this.balance -= amount;
}
}
const laylaWallet = new Wallet("ليلى");
const karimWallet = new Wallet("كريم");
laylaWallet.deposit(200);
laylaWallet.withdraw(50);
console.log(laylaWallet.balance);
console.log(karimWallet.balance);
الناتج
150
0

المحفظتان من القالب نفسه، لكن رصيد ليلى لا علاقة له برصيد كريم. والدوال تحمل قواعد التعامل مع البيانات: لا إيداع بمبلغ سالب، ولا سحب بأكثر من الرصيد.

في المحفظة السابقة ثغرة: أي سطر في البرنامج يستطيع أن يكتب laylaWallet.balance = 1000000 ويتجاوز كل القواعد. لمنع ذلك نجعل الخاصية خاصة (Private) بوضع # قبل اسمها، فلا يراها إلا الكود الموجود داخل الكلاس:

class SafeWallet {
#balance = 0;
deposit(amount) {
if (amount <= 0) {
throw new Error("المبلغ يجب أن يكون أكبر من صفر");
}
this.#balance += amount;
}
get balance() {
return this.#balance;
}
}
const safe = new SafeWallet();
safe.deposit(300);
console.log(safe.balance);
safe.balance = 999999;
console.log(safe.balance);
الناتج
300
300
  • #balance = 0: خاصية خاصة قيمتها الابتدائية صفر. وكتابة safe.#balance خارج الكلاس خطأ في الصياغة يمنع تشغيل الكود.
  • get balance(): دالة قراءة (Getter). تستدعيها كأنها خاصية عادية وبدون قوسين: safe.balance.

ولأننا كتبنا دالة قراءة فقط، فمحاولة تغيير الرصيد من الخارج لم تغيّر شيئاً. الطريق الوحيد لزيادته هو deposit بقواعدها.

بعض الدوال تخص الكلاس كله وليس كائناً بعينه. نضع قبلها static ونستدعيها من اسم الكلاس مباشرة، بدون new:

class Temperature {
static toFahrenheit(celsius) {
return celsius * 9 / 5 + 32;
}
}
console.log(Temperature.toFahrenheit(30));
الناتج
86

وقد استخدمت دوالاً ثابتة كثيراً دون أن تعرف اسمها: Math.max و JSON.parse و Date.now و Promise.all.

أحياناً تحتاج كلاساً يشبه كلاساً موجوداً مع بعض الإضافات. بدلاً من نسخ الكود، اجعل الجديد يرث من القديم بكلمة extends:

class Character {
constructor(name, health) {
this.name = name;
this.health = health;
}
attack() {
return `${this.name} يضرب بالسيف`;
}
takeDamage(points) {
this.health -= points;
return `${this.name}: بقي ${this.health} من الصحة`;
}
}
class Wizard extends Character {
constructor(name, health, mana) {
super(name, health);
this.mana = mana;
}
attack() {
this.mana -= 10;
return `${this.name} يلقي كرة نار`;
}
}
const knight = new Character("الفارس", 100);
const wizard = new Wizard("الساحر", 70, 50);
console.log(knight.attack());
console.log(wizard.attack());
console.log(wizard.takeDamage(20));
console.log(wizard.mana);
console.log(wizard instanceof Character);
الناتج
الفارس يضرب بالسيف
الساحر يلقي كرة نار
الساحر: بقي 50 من الصحة
40
true
  • Wizard extends Character: الساحر شخصية، فيأخذ كل خصائصها ودوالها. لذلك عملت takeDamage معه مع أننا لم نكتبها في كلاسه.
  • super(name, health): تستدعي constructor الكلاس الأب ليجهّز الخصائص المشتركة.
  • attack كُتبت من جديد في Wizard، فاستُخدمت نسخة الابن بدلاً من نسخة الأب.
  • instanceof تسأل: هل هذا الكائن من هذا الكلاس أو من أحد أبنائه؟
جرّب بنفسكJavaScript
أو اضغط Ctrl + Enter

الناتج

أضف دالة remove(name) تحذف المنتج الذي يحمل هذا الاسم (تذكّر filter). ثم أضف دالة قراءة باسم mostExpensive تُرجع اسم أغلى منتج في السلة.

  • نسيان new: كتابة Student("سارة", 9) بدونها تسبب الخطأ Class constructor Student cannot be invoked without 'new'.
  • نسيان this داخل الدوال: كتابة balance += amount بدلاً من this.balance += amount تبحث عن متغير عادي اسمه balance فلا تجده، ويظهر ReferenceError.
  • وضع قوسين بعد دالة القراءة: safe.balance() خطأ، لأن balance تُقرأ كخاصية: safe.balance.

سؤال

متى تُنفَّذ الدالة constructor؟

سؤال

ما وظيفة super(name) داخل constructor الكلاس الابن؟

  • الكلاس قالب تصنع منه كائنات بـ new، ولكل كائن بياناته المستقلة.
  • constructor تجهّز الخصائص، و this تشير إلى الكائن الحالي.
  • علامة # تجعل الخاصية خاصة لا تُلمس من خارج الكلاس، و get تسمح بقراءتها بأمان.
  • static لدوال تُستدعى من اسم الكلاس نفسه مثل Math.max.
  • extends تورّث كلاساً من آخر، و super تستدعي الأب.