الوراثة في لغة JavaScript


الوراثة في لغة JavaScript

يا هلا بالشباب! اليوم بنتكلم عن موضوع مهم جداً في JavaScript وهو الوراثة (Inheritance). يمكن لو جاي من لغات زي Java أو C++ تكون متعود على كلاسات وأشياء زي كذا، لكن في JavaScript الموضوع مختلف شويتين، ومبني على حاجة اسمها 'النماذج الأولية' أو Prototypes.

الوراثة القائمة على النماذج الأولية (Prototype-based Inheritance)

في JavaScript، ما عندنا وراثة كلاسات بالمعنى التقليدي قبل ES6. الوراثة هنا تعتمد على الـ Prototypes. كل أوبجكت في JavaScript عنده 'نموذج أولي' (Prototype) بيورث منه الخصائص والدوال. تقدر تعتبره كأنه أب روحي للأوبجكت.

كيف تشوف الـ Prototype؟ فيه خاصية اسمها __proto__ (لاحظ الشرطتين اللي تحت) أو تقدر تستخدم Object.getPrototypeOf().

ملاحظة: الـ __proto__ هي طريقة قديمة وممكن ما تكون موجودة في كل البيئات، الأفضل استخدام Object.getPrototypeOf() أو Object.setPrototypeOf().

شوف هذا المثال البسيط:

const animal = {
  makeSound() {
    console.log('Some generic sound');
  }
};

const dog = {
  bark() {
    console.log('Woof!');
  }
};

// Dog inherits from animal
Object.setPrototypeOf(dog, animal);

dog.makeSound(); // Output: Some generic sound
dog.bark();      // Output: Woof!

console.log(Object.getPrototypeOf(dog) === animal); // Output: true

هنا، dog ورث دالة makeSound من animal. هذا هو جوهر الوراثة القائمة على الـ Prototypes.

الوراثة باستخدام دوال البناء (Constructor Functions)

قبل ظهور الكلاسات في ES6، كنا نستخدم دوال البناء (Constructor Functions) عشان نسوي 'كلاسات' ووراثة.

function Animal(name) {
  this.name = name;
}

Animal.prototype.makeSound = 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} makes a sound.</code>);
};

function Dog(name, breed) {
  Animal.call(this, name); // Call the parent constructor
  this.breed = breed;
}

// Set up the prototype chain
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog; // Important: Reset constructor reference

Dog.prototype.bark = 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} barks! It's a ${this.breed}.</code>);
};

const myDog = new Dog('Buddy', 'Golden Retriever');
myDog.makeSound(); // Output: Buddy makes a sound.
myDog.bark();      // Output: Buddy barks! It's a Golden Retriever.

console.log(myDog instanceof Dog);    // true
console.log(myDog instanceof Animal); // true

تذكر: Animal.call(this, name) تستخدم لاستدعاء دالة البناء الأب في سياق الكائن الحالي (this)، وهذا عشان نورث الخصائص اللي داخل الـ constructor.

و Dog.prototype = Object.create(Animal.prototype) هي الطريقة الصحيحة لربط الـ Prototype chain بدون ما ننسخ الـ Animal.prototype نفسه، وبالتالي أي تغييرات في Animal.prototype ما تأثر على Dog.prototype مباشرة.

Dog.prototype.constructor = Dog; مهم عشان الـ constructor الخاص بالكائن يشير إلى Dog بدلاً من Animal.

وراثة الكلاسات في ES6 (Syntactic Sugar)

مع ES6، جابوا لنا class و extends اللي تخلي الوراثة شكلها أقرب للغات الثانية، لكن في الحقيقة، هي مجرد 'سكر نحوي' (Syntactic Sugar) فوق الوراثة القائمة على الـ Prototypes اللي شفناها قبل شوي. يعني ما غيرت طريقة عمل JavaScript الأساسية، بس سهلت علينا الكتابة.

class Animal {
  constructor(name) {
    this.name = name;
  }

  makeSound() {
    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} makes a sound.</code>);
  }

  static describe() {
    console.log('This is a generic animal class.');
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name); // Call the parent class constructor
    this.breed = breed;
  }

  bark() {
    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} barks! It's a ${this.breed}.</code>);
  }

  // Override parent method
  makeSound() {
    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} howls and barks!</code>);
  }
}

const myCat = new Animal('Whiskers');
myCat.makeSound(); // Output: Whiskers makes a sound.

const myNewDog = new Dog('Max', 'Labrador');
myNewDog.makeSound(); // Output: Max howls and barks! (Overridden method)
myNewDog.bark();      // Output: Max barks! It's a Labrador.

Animal.describe(); // Output: This is a generic animal class.
// myNewDog.describe(); // Error: describe is not a function (static method)

الـ super(name) داخل constructor الكلاس الابن ضرورية جداً. هي تستدعي constructor الكلاس الأب عشان تهيئ الخصائص اللي ورثتها منه. لازم تكون أول سطر في constructor الكلاس الابن.

وكمان، تقدر تسوي override لدوال الأب (مثل makeSound هنا) عشان تعطيها سلوك خاص بالكلاس الابن.

الـ static methods تكون خاصة بالكلاس نفسه مو بالكائنات اللي تنشئها منه. يعني تستدعيها باسم الكلاس مباشرة.

خلاصة الكلام

الوراثة في JavaScript مبنية على الـ Prototypes في الأساس. الكلاسات في ES6 سهلت كتابة الكود وخليته أوضح، لكنها في النهاية ما غيرت المبدأ الأساسي. فهمك للـ Prototypes بيخليك مبرمج JavaScript أقوى وأفهم لطريقة عمل اللغة.

أتمنى يكون الدرس خفيف ومفيد لكم! نشوفكم في درس ثاني.