الوراثة في لغة JavaScript
أهلاً يا بطل! خلينا ندخل في الموضوع على طول. الوراثة (Inheritance) في جافاسكريبت هي طريقة تخلي كائن ياخذ خصائص ووظائف من كائن ثاني. يعني بدل ما تعيد كتابة نفس الكود، تقدر تورّثه وتعدّل عليه أو تضيف له.
الوراثة القائمة على النموذج الأولي (Prototypal Inheritance)
في جافاسكريبت، ما عندنا وراثة كلاسيكية زي لغات ثانية (قبل ES6). هنا الشغل كله على النماذج الأولية (Prototypes). كل كائن في جافاسكريبت عنده نموذج أولي (Prototype) يشير لكائن ثاني، وهذا الكائن الثاني ممكن يكون عنده نموذج أولي خاص فيه، وهكذا تتكون سلسلة اسمها سلسلة النموذج الأولي (Prototype Chain). لما تحاول توصل لخاصية أو دالة في كائن، جافاسكريبت تدور عليها أول شيء في الكائن نفسه، إذا ما لقتها تدور في النموذج الأولي حقه، وإذا ما لقتها تدور في النموذج الأولي للنموذج الأولي، وهكذا لين توصل لـ null.
مثال بسيط: Object.create()
شوف هالمثال كيف نسوي كائن يورث من كائن ثاني بكل بساطة:
const car = {
wheels: 4,
drive() {
console.log("Driving a car!");
}
};
const tesla = Object.create(car);
tesla.model = "Model S";
tesla.drive = function() {
console.log(<code dir="ltr" style="background:#f3f4f6; color:#0056b3; padding:2px 6px; border-radius:4px; font-family:monospace; direction:ltr !important; display:inline-block;">Driving a ${this.model} with ${this.wheels} wheels.</code>);
};
console.log(tesla.wheels); // 4 (ورثها من car)
tesla.drive(); // Driving a Model S with 4 wheels. (الدالة الخاصة بـ tesla هي اللي اشتغلت)
car.drive(); // Driving a car!
ملاحظة: لما سوينا Object.create(car)، الكائن tesla صار عنده car كنموذج أولي له. لما نادينا tesla.wheels، ما لقاها في tesla نفسه فراح دور عليها في النموذج الأولي حقه (اللي هو car) ولقاها هناك.
دوال البناء (Constructor Functions) والـ prototype
قبل ES6، كنا نستخدم دوال البناء عشان نسوي "فئات" (Classes) ونورث منها. الفكرة إن كل دالة بناء عندها خاصية اسمها prototype، وهذي الخاصية هي اللي تحتوي على الخصائص والدوال اللي نبغى الكائنات اللي تتكون من دالة البناء هذي تورثها.
مثال: وراثة باستخدام دوال البناء
// دالة البناء الأساسية (الأب)
function Person(name, age) {
this.name = name;
this.age = age;
}
// إضافة دالة للنموذج الأولي لـ Person
Person.prototype.greet = function() {
console.log(<code dir="ltr" style="background:#f3f4f6; color:#0056b3; padding:2px 6px; border-radius:4px; font-family:monospace; direction:ltr !important; display:inline-block;">Hello, my name is ${this.name} and I am ${this.age} years old.</code>);
};
// دالة البناء الفرعية (الابن)
function Employee(name, age, jobTitle) {
// نستدعي دالة البناء للأب عشان نورث خصائصها
Person.call(this, name, age);
this.jobTitle = jobTitle;
}
// عشان Employee يورث دوال وخصائص Person.prototype
// لازم نخلي Employee.prototype هو نسخة من Person.prototype
Employee.prototype = Object.create(Person.prototype);
// مهم: نرجع نضبط constructor عشان يشير لـ Employee
Employee.prototype.constructor = Employee;
// إضافة دالة خاصة بـ Employee
Employee.prototype.work = function() {
console.log(<code dir="ltr" style="background:#f3f4f6; color:#0056b3; padding:2px 6px; border-radius:4px; font-family:monospace; direction:ltr !important; display:inline-block;">${this.name} is working as a ${this.jobTitle}.</code>);
};
// ممكن نعدل على دالة greet الخاصة بالأب
Employee.prototype.greet = function() {
console.log(<code dir="ltr" style="background:#f3f4f6; color:#0056b3; padding:2px 6px; border-radius:4px; font-family:monospace; direction:ltr !important; display:inline-block;">Hi, I'm ${this.name}, ${this.age} years old, and my job is ${this.jobTitle}.</code>);
};
const ali = new Person("Ali", 30);
const sara = new Employee("Sara", 25, "Software Engineer");
ali.greet(); // Hello, my name is Ali and I am 30 years old.
sara.greet(); // Hi, I'm Sara, 25 years old, and my job is Software Engineer.
sara.work(); // Sara is working as a Software Engineer.
console.log(sara instanceof Employee); // true
console.log(sara instanceof Person); // true
شرح سريع:
Person.call(this, name, age): هذي وظيفتها كأنها تستدعي دالة بناء الأب Person بس في سياق الكائن الحالي this (اللي هو sara في هالحالة).
Employee.prototype = Object.create(Person.prototype): هذي هي الحركة الأساسية للوراثة. تخلي النموذج الأولي حق Employee يشير للنموذج الأولي حق Person. يعني أي دالة أو خاصية موجودة في Person.prototype راح تكون متوفرة لكائنات Employee.
Employee.prototype.constructor = Employee: هذي خطوة مهمة عشان ما يتخربط الـ constructor حق Employee ويشير لـ Person.
الـ Classes في ES6 (سكر نحوي)
مع ES6، جافاسكريبت جابت لنا طريقة أسهل وأوضح لكتابة الوراثة باستخدام كلمة class و extends. هذي ما غيرت طريقة عمل الوراثة (لازالت قائمة على النماذج الأولية في الخلفية)، بس سهلت علينا الكتابة والقراءة.
مثال: وراثة باستخدام Classes
// الكلاس الأب
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
console.log(<code dir="ltr" style="background:#f3f4f6; color:#0056b3; padding:2px 6px; border-radius:4px; font-family:monospace; direction:ltr !important; display:inline-block;">Hello, my name is ${this.name} and I am ${this.age} years old.</code>);
}
}
// الكلاس الابن يورث من Person
class Employee extends Person {
constructor(name, age, jobTitle) {
// نستدعي constructor الأب باستخدام super()
super(name, age);
this.jobTitle = jobTitle;
}
work() {
console.log(<code dir="ltr" style="background:#f3f4f6; color:#0056b3; padding:2px 6px; border-radius:4px; font-family:monospace; direction:ltr !important; display:inline-block;">${this.name} is working as a ${this.jobTitle}.</code>);
}
// نعدل على دالة greet (Override)
greet() {
// ممكن نستدعي دالة greet الأب باستخدام super.greet()
// super.greet();
console.log(<code dir="ltr" style="background:#f3f4f6; color:#0056b3; padding:2px 6px; border-radius:4px; font-family:monospace; direction:ltr !important; display:inline-block;">Hi, I'm ${this.name}, ${this.age} years old, and my job is ${this.jobTitle}.</code>);
}
}
const omar = new Person("Omar", 40);
const layla = new Employee("Layla", 28, "Product Manager");
omar.greet(); // Hello, my name is Omar and I am 40 years old.
layla.greet(); // Hi, I'm Layla, 28 years old, and my job is Product Manager.
layla.work(); // Layla is working as a Product Manager.
توضيح:
extends Person: هذي الكلمة هي اللي تخلي Employee يورث من Person.
super(name, age): داخل الـ constructor حق الكلاس الابن، لازم تستدعي super() قبل ما تستخدم this. هذي كأنها تستدعي constructor الكلاس الأب.
super.greet(): لو بغيت تستدعي دالة من الكلاس الأب داخل دالة بنفس الاسم في الكلاس الابن، تستخدم super.اسم_الدالة().
متى تستخدم أي طريقة؟
في الغالب، مع ظهور ES6، الأفضل والأكثر شيوعاً هو استخدام الـ class و extends لأنها أوضح وأسهل للقراءة والكتابة، خصوصاً لو كنت جاي من لغات ثانية فيها مفهوم الكلاسات. لكن مهم جداً تفهم إن الأساس اللي تحتها هو الوراثة القائمة على النماذج الأولية.
كذا نكون خلصنا أساسيات الوراثة في جافاسكريبت. الموضوع أعمق من كذا بس هذي البداية عشان تفهم كيف تشتغل الأمور. بالتوفيق!