الكلاسات في JavaScript: class والوراثة
تعلمت أن تنشئ كائناً بكتابة خصائصه بين قوسين معقوفين. لكن ماذا لو احتاج برنامجك مئة طالب، لكل واحد الخصائص نفسها والدوال نفسها؟ لن تكتب الكائن مئة مرة. في هذا الدرس ستتعلم الكلاس (Class): قالب تكتبه مرة واحدة، ثم تصنع منه ما تشاء من الكائنات.
ما هو الكلاس؟
Section titled “ما هو الكلاس؟”الكلاس وصف لنوع من الكائنات: ما الخصائص التي يحملها، وما الذي يستطيع فعله.
تخيّل قالب الكعك: القالب نفسه لا يؤكل، لكنك تصنع به عشرات الكعكات، لها الشكل نفسه، ولكل واحدة حشوتها. القالب هو الكلاس، وكل كعكة كائن صُنع منه.
أول كلاس
Section titled “أول كلاس”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.
كل كائن له بياناته
Section titled “كل كائن له بياناته”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);1500المحفظتان من القالب نفسه، لكن رصيد ليلى لا علاقة له برصيد كريم. والدوال تحمل قواعد التعامل مع البيانات: لا إيداع بمبلغ سالب، ولا سحب بأكثر من الرصيد.
الخصائص الخاصة: علامة
Section titled “الخصائص الخاصة: علامة”في المحفظة السابقة ثغرة: أي سطر في البرنامج يستطيع أن يكتب 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);300300#balance = 0: خاصية خاصة قيمتها الابتدائية صفر. وكتابةsafe.#balanceخارج الكلاس خطأ في الصياغة يمنع تشغيل الكود.get balance(): دالة قراءة (Getter). تستدعيها كأنها خاصية عادية وبدون قوسين:safe.balance.
ولأننا كتبنا دالة قراءة فقط، فمحاولة تغيير الرصيد من الخارج لم تغيّر شيئاً. الطريق الوحيد لزيادته هو deposit بقواعدها.
الدوال الثابتة: static
Section titled “الدوال الثابتة: static”بعض الدوال تخص الكلاس كله وليس كائناً بعينه. نضع قبلها 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 و super
Section titled “الوراثة: extends و super”أحياناً تحتاج كلاساً يشبه كلاساً موجوداً مع بعض الإضافات. بدلاً من نسخ الكود، اجعل الجديد يرث من القديم بكلمة 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 من الصحة40trueWizard extends Character: الساحر شخصية، فيأخذ كل خصائصها ودوالها. لذلك عملتtakeDamageمعه مع أننا لم نكتبها في كلاسه.super(name, health): تستدعيconstructorالكلاس الأب ليجهّز الخصائص المشتركة.attackكُتبت من جديد فيWizard، فاستُخدمت نسخة الابن بدلاً من نسخة الأب.instanceofتسأل: هل هذا الكائن من هذا الكلاس أو من أحد أبنائه؟
جرّب بنفسك
Section titled “جرّب بنفسك”الناتج
أضف دالة remove(name) تحذف المنتج الذي يحمل هذا الاسم (تذكّر filter). ثم أضف دالة قراءة باسم mostExpensive تُرجع اسم أغلى منتج في السلة.
أخطاء شائعة
Section titled “أخطاء شائعة”- نسيان
new: كتابةStudent("سارة", 9)بدونها تسبب الخطأClass constructor Student cannot be invoked without 'new'. - نسيان
thisداخل الدوال: كتابةbalance += amountبدلاً منthis.balance += amountتبحث عن متغير عادي اسمهbalanceفلا تجده، ويظهرReferenceError. - وضع قوسين بعد دالة القراءة:
safe.balance()خطأ، لأنbalanceتُقرأ كخاصية:safe.balance.
اختبر نفسك
Section titled “اختبر نفسك”سؤال
متى تُنفَّذ الدالة constructor؟
constructor تعمل مرة واحدة لكل كائن لحظة إنشائه بـ new، وتجهّز خصائصه الابتدائية.
سؤال
ما وظيفة super(name) داخل constructor الكلاس الابن؟
super(...) تشغّل constructor الأب على الكائن الجديد نفسه، ويجب أن تأتي قبل استخدام this.
الخلاصة
Section titled “الخلاصة”- الكلاس قالب تصنع منه كائنات بـ
new، ولكل كائن بياناته المستقلة. constructorتجهّز الخصائص، وthisتشير إلى الكائن الحالي.- علامة
#تجعل الخاصية خاصة لا تُلمس من خارج الكلاس، وgetتسمح بقراءتها بأمان. staticلدوال تُستدعى من اسم الكلاس نفسه مثلMath.max.extendsتورّث كلاساً من آخر، وsuperتستدعي الأب.