دليل تعلم جافاسكربت وراثة الأصناف (Class inheritance) في جافاسكربت


صفا الفليج

تُعدّ وراثة الأصناف واحدةً من الطرائق لتوسعة أحد الأصناف لديك، أي أن نقدّم وظائف جديدة لأحد الأصناف علاوةً على ما لديه.

عبارة التوسعة extends

لنقل بأنّ لدينا صنف الحيوان Animal:

class Animal {
  constructor(name) {
    this.speed = 0;
    this.name = name;
  }
  run(speed) {
    this.speed += speed;
    alert(`${this.name} runs with speed ${this.speed}.`); // يركض حيوان كذا بالسرعة كذا
  }
  stop() {
    this.speed = 0;
    alert(`${this.name} stands still.`); // يقف حيوان كذا في مكانه
  }
}

let animal = new Animal("My animal");

هكذا نصف كائن الحيوان animal وصنف الحيوان Animal في صورة:

rabbit-animal-independent-animal.png

ولنقل بأنّنا نريد إضافة صنف آخر… ليكن أرنبًا class Rabbit.

وطبعًا، فالأرانب حيوانات أيضًا، وعلى صنف الأرانب Rabbit أن يكون أساسه الحيوان Animal ليصل إلى توابِع الحيوانات فتقوم الأرانب بما تقوم به الحيوانات ”العادية“ (generic).

صياغة توسعة الصنف إلى صنف آخر هي: class Child extends Parent (صنف الابن توسعة من صنف الأبّ).

لنصنع صنف الأرنب class Rabbit ليرث الحيوان Animal:

class Rabbit extends Animal { // لاحظ
  hide() {
    alert(`${this.name} hides!`); // اختفى هكذا!
  }
}

let rabbit = new Rabbit("White Rabbit"); // الأرنب الأبيض

rabbit.run(5); // White Rabbit runs with speed 5. // يركض حيوان الأرنب بسرعة 5
rabbit.hide(); // White Rabbit hides! // اختفى الأرنب الأبيض!

يمكن لكائن Rabbit الوصول إلى توابِع Rabbit (مثل rabbit.hide()) كما توابِع Animal (مثل rabbit.run()).

داخليًا فعبارة extends تعمل كما تعمل ميكانيكية كائنات prototype المعهودة، فضتبط Rabbit.prototype.[[Prototype]]‎ ليكون Animal.prototype. بهذا لو لم يوجد التابِع في كائن Rabbit.prototype، يأخذه المحرّك من Animal.prototype.

animal-rabbit-extends.png

فمثلًا لنجد التابِع rabbit.run يبحث المحرّك (من أسفل إلى أعلى، كما الصورة):

  1. كائن الأرنب rabbit (ليس فيه run).
  2. كائن prototype له، أي Rabbit.prototype (فيه hide وليس فيه run).
  3. كائن prototype له، أي (بسبب extends) ‏Animal.prototype، بهذا يجد تابِع run أخيرًا.

كما نذكر من فصل ”“، فمحرّك جافاسكربت نفسه يستعمل التوارث عبر prototype لكائناته المضمّنة في اللغة. فمثلًا كائن Date.prototype.[[Prototype]]‎ هو الكائن Object.prototype. لهذا يمكن للتواريخ الوصول إلى توابِع الكائنات العادية.

ملاحظة: تسمح صياغة الصنف ليس بتحديد الصنف فقط وإنما إضافة أي تعبير بعد عبارة extends. فمثلًا استدعاء التابع التاي سيبني صنف الأب.

function f(phrase) {
  return class {
    sayHi() { alert(phrase) }
  }
}

class User extends f("Hello") {}

new User().sayHi(); // Hello

سيرثُ الصنف class User من النتيجة للصنف f("Hello")‎.

وهذه الميزة مفيدة لكتابة الأنماط البرمجية المتقدمة عندما نستخدم تابع لينشئ صنف صنف اعتمادًا على عدة شروط والتي يمكن أن ترثها.

إعادة تعريف دالة

الآن نتحرّك ونعيد تعريف أحد التوابِع. مبدئيًا فكلّ التوابِع غير المحدّدة في صنف الأرانب تُؤخذ مباشرةً ”كما هي“ من صنف الحيوانات.

ولكن لو حدّدنا تابِعًا معيّنًا في في Rabbit (وليكن stop()‎) فسيُستعمل بدله:

class Rabbit extends Animal {
  stop() {
    // الآن سنسخدمها من أجل ‫rabbit.stop‪()
// بدلًا من استخدام ‫stop()‎ من الصنف مباشرة

  }
}

عادةً لا نرغب باستبدال كامل ما في التابِع الأب، بل البناء فوقه أو تعديله أو توسعة وظائفه، أي ننفّذ شيئًا في التابِع ثمّ نستدعي التابِع الأبّ قبل أو بعد ذلك.

ولحسن الحظ فالأصناف تقدّم لنا عبارة "super".

  • نستعمل super.method(...)‎ لنستدعي تابِعًا أبًا.
  • ونستدعي super(...)‎ لنستدعي الباني الأب (هذا فقط لو كنّا في باني هذه الدالة).

فمثلًا، ليختبئ هذا الأرنب النبه ما إن يتوقّف:

class Animal {

  constructor(name) {
    this.speed = 0;
    this.name = name;
  }

  run(speed) {
    this.speed += speed;
    alert(`${this.name} runs with speed ${this.speed}.`);
  }

  stop() {
    this.speed = 0;
    alert(`${this.name} stands still.`);
  }

}

class Rabbit extends Animal {
  hide() {
    alert(`${this.name} hides!`);
  }

  // هنا
  stop() {
    super.stop(); // نستدعي ‫stop في الأب
    this.hide(); // ثمّ نستدعي ‫hide
  }
}

let rabbit = new Rabbit("White Rabbit");

rabbit.run(5); // سيركض ‫ "White Rabbit" بسرعة 5
rabbit.stop(); // توقف الأرنب .وهو الآن مختبئ

الآن صار داخل الأرنب التابِع Rabbit الذي يستدعي التابِع stop من أباه

كما شرحنا في درس "الدوال السهمية" لا يمكننا استخدام الكلمة المفتاحية super على الدوالّ السهمية.

ولو استطعنا الوصول إليها من خلال الكلمة المفتاحية super فستكون مأخوذة من الدالّة الخارجية. هكذا:

class Rabbit extends Animal {
  stop() {
    setTimeout(() => super.stop(), 1000); // استدعاء الأب سيتوقف بعد ثانية واحدة
  }
}

إن عمل الدلّة stop()‎ مشابه تمامًا لعمل الكلمة المفتاحية super مع الدوال السهمية. لذا فإنها تعمل مثلما نريد. ولو حُددت كدالّة عادية فسيظهر لدينا خطأ:

// Unexpected super
setTimeout(function() { super.stop() }, 1000);

إعادة تعريف الباني

متى تعاملنا مع البانيات، صار الأمر

لم يكن لصنف الأرنب Rabbit (حتّى اللحظة) أيّ بانٍ له.

حسبما تقول المواصفة فلو وسّع أحد الأصناف صنفًا آخر ليس له بانيًا، فسيُولّد المحرّك هذا الباني ”الفارغ“:

class Rabbit extends Animal {
  // يُولّد للأصناف التي تُوسِّع أخرى وليس فيها بانيات
  constructor(...args) {
    super(...args);
  }
}

كما نرى فهي تستدعي الباني constructor الأبّ بتمرير كلّ الوُسطاء إليه، فقط. لا يحدث هذا إلّا لو لم نكتب بانيًا في الصنف الابن.

لنُضف الآن بانيًا من عندنا إلى الأرنب Rabbit. سيضبط هذا الباني الخاصية earLength علاوةً على name:

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

class Rabbit extends Animal {

  // هنا  constructor(name, earLength) {
    this.speed = 0;
    this.name = name;
    this.earLength = earLength;
  }

  // ...
}

// لا تعمل!
let rabbit = new Rabbit("White Rabbit", 10); // Error: this is not defined.

ماذا؟! هناك خطأ! انتهى الأمر، ”صناعة الأرانب“ لم تعد ممكنة بعد الآن. تراه ما المشكلة؟

باختصار: على البانيات في الأصناف الموروثة استدعاء super(...)‎ هذا أولًا، وثانيًا استدعاءه قبل استعمال this.

ولكن لحظة… لماذا؟ ما الذي يجري؟ هذا المطلب غريب حقًا.

بالطبع لا شيء بدون توضيح وشرح، لذا فلنتعمّق داخل التفاصيل ونفهم ما يجري ”حبّة حبّة“.

تضع لغة جافاسكربت خطًا فاصلًا بين الدالة البانية للصنف الموروث (أي ”الباني المشتقّ“) وغيرها من دوال. لهذا الباني خاصية داخلية فريدة اسمها ‎[[ConstructorKind]]:"derived"‎، وهي علامة يضعها المحرّك عليه داخليًا خلف الكواليس. تؤثّر هذه ”العلامة“ على سلوك الباني حين نستعمله مع new.

  • حين نُنفّذ الدوال العادية باستعمال new، تُنشِئ لنا كائنًا فارغًا وتضبطه ليكون this.
  • ولكن حين يعمل الباني المشتق، فلا يفعل ذلك، بل يتوقّع من الباني الأبّ القيام بهذه المهمة الصعبة.

لذا على الباني المشتق استدعاء super ليُنفّذ باني أباه (غير المشتق) وإلّا فلن يُنشأ أيّ كائن يكون this، بهذا تكون الشيفرة خطأ.

على باني الصنف Rabbit استدعاء super()‎ قبل this ليعمل، هكذا تمامًا:

class Animal {

  constructor(name) {
    this.speed = 0;
    this.name = name;
  }

  // ...
}

class Rabbit extends Animal {

  constructor(name, earLength) {
    super(name); // هنا
    this.earLength = earLength;
  }

  // ...
}

// الآن كلّ شيء كما يجب أن يكون
let rabbit = new Rabbit("White Rabbit", 10);
alert(rabbit.name); // White Rabbit
alert(rabbit.earLength); // 10

عبارة super: أمور داخلية و[[HomeObject]]

تحذير: سترى معلومات متقدّمة لو كنت تقرأ هذا الدرس أوّل مرّة فيمكنك تخطّي هذا الجزء، إذ يتكلّم عن الميكانيكا الداخلية التي يستعملاها التوارث وعبارة super.

هيًا ننزل إلى الأعماق ونرى ما خلف كواليس super. في هذه الرحلة سنرى أمور جميلة أيضًا ”إكسترا سوبر“.

لنوضّحها من البداية: لو أخذت كلّ ما تعلّمناه حتّى الحظة، فما من طريقة لتعمل فيها عبارة super في أيّ حال من الأحوال!

تمامًا، كما فكّرت الآن، لنطرح السؤال: كيف تعمل هذه العبارة تقنيًا أساسًا؟ متى ما عمل أحد توابِع الكائنات، جلب الكائن الحالي على أنّه this. فلو استدعينا super.method()‎ فكلّ ما على المحرّك فعله هو جلب التابِع method من كائن prototype للكائن الحالي، صحيح؟ أجل ولكن كيف ذلك؟

ربّما ترى المهمة سهلة ولكنّها ليست كذلك البتة. يمكن القول أنّ المحرّك يعلم بالكائن الحالي this، فيمكنه أن يأخذ تابِع method في الأب باستعمال this.__proto__.method. ولكن للأسف فهذا الحلّ ”البسيط“ لن يعمل أبدًا.

لنوضّح المشكلة أولًا، باستعاضة الأصناف لتكون كائنات عادية لتسهيل الفهم.

(لو لم تريد معرفة التفاصيل فتخطّى هذا القسم وانتقل إلى الجزء [[HomeObject]]، لا مشكلة. أو واصِل معنا في هذه الرحلة الموحشة في أعماق غابة لغة جافاسكربت.)

في المثال أسفله، نرى rabbit.__proto__ = animal. لنجرّب الآن هذا الأمر: داخل rabbit.eat()‎ نستدعي animal.eat()‎ باستعمال this.__proto__‎:

let animal = {
  name: "Animal",
  eat() {
    alert(`${this.name} eats.`);
  }
};

let rabbit = {
  __proto__: animal,
  name: "Rabbit",
  eat() {
    // هذه إحدى طرق عمل super.eat()
    this.__proto__.eat.call(this); // (*)
  }
};

rabbit.eat(); // Rabbit eats.

أخذنا في السطر (*) التابِعَ eat من كائن prototype ‏(animal) واستدعيناه على أنّ السياق هو الكائن الحالي. لاحظ أهمية ‎.call(this)‎ إذ لو كتبنا this.__proto__.eat()‎ فقط فسيُنفّذ التابِع eat الأب داخل سياق كائنَ prototype، وليس في الكائن الحالي.

وفي الجزء الأول من الشيفرة نرى كلّ شيء يعمل: عمل التابِع alert كما ينبغي عليه.

حان وقت إضافة كائن آخر إلى السلسلة، وكسر هذه السلسلة إربًا:

let animal = {
  name: "Animal",
  eat() {
    alert(`${this.name} eats.`);
  }
};

let rabbit = {
  __proto__: animal,
  eat() {
    // ...هنا يأكل الأرنب كما تأكل الأرانب، بعدها نستدعي التابِع الأبّ (animal)
    this.__proto__.eat.call(this); // (*)
  }
};

let longEar = {
  __proto__: rabbit,
  eat() {
    // ...الأرنب ذو الأذن الطويلة يلهو ويلعب، ثمّ نستدعي التابِع الأبّ (rabbit)
    this.__proto__.eat.call(this); // (**)
  }
};

// هنا!longEar.eat(); // Error: Maximum call stack size exceeded

لم تعد الشيفرة تعمل الآن! نرى خطأً عند استدعاء longEar.eat()‎.

قد لا يبدو الأمر جليًا من أوّل نظرة، ولكن لو تعقّبنا استدعاء longEar.eat()‎ فسنرى الأمر بوضوح، ففي السطرين () و (*) تكون قيمة this هي الكائن الحالي (longEar). هذا ضمن الأساسيات، فعلى توابِع الكائنات جلب الكائن الحالي فهو this، وليس كائنَ prototype أو ما شابهه.

بذلك في السطرين معًا (*) و (**) تكون قيمة this.__proto__‎ واحدة: الكائن rabbit، وكلاهما يستدعيان التابِع rabbit.eat دون أن يرتقيا بالسلسلة، فيدوران في حلقة لا نهاية لها.

إليك عمّا يحدث في صورة:

this-super-loop.png

نرى في التابِع longEar.eat()‎ عند السطر (**) استدعاء rabbit.eat بتمرير this=longEar.

    // نرى داخل ‫ longEar.eat()‎ قيمة this = longEar
    this.__proto__.eat.call(this) // (**)
    // يصير
    longEar.__proto__.eat.call(this)
    // وهو فعليًا
    rabbit.eat.call(this);
  1. وبعدها في السطر (*) داخل rabbit.eat، نحاول تمرير الاستدعاء إلى مستوًى أعلى داخل السلسلة، ولكن قيمة this=longEar، بهذا تصير قيمة this.__proto__.eat هي rabbit.eat ثانيةً.
    // نرى داخل ‫ rabbit.eat()‎ قيمة this = longEar
    this.__proto__.eat.call(this) // (*)
    // becomes
    longEar.__proto__.eat.call(this)
    // or (again)
    rabbit.eat.call(this);
  1. بهذا… يستدعي rabbit.eat نفسه في حلقة لانهاية لها لأنّها يعجز عن الارتقاء في السلسلة.

ما من طريقة لحلّ هذه المشكلة باستعمال this فقط.

[[HomeObject]]

أضافت لغة جافاسكربت -لحلّ هذه المعضلة- خاصية داخلية (أخرى) للدوال، وهي ”الكائن المنزل“ [[HomeObject]].

متى ما حُدّدت الدالة لتكون صنفًا أو تابِعًا لكائن، أصبحت خاصية [[HomeObject]] للدالة ذلك الصنف أو الكائن.

تستعمل super هذا الكائن لحلّ كائن prototype الأبّ هو وتوابِعه.

لنرى كيف يعمل هذا الشيء، بالكائنات العادية أولًا:

let animal = {
  name: "Animal",
  eat() {         // animal.eat.[[HomeObject]] == animal
    alert(`${this.name} eats.`);
  }
};

let rabbit = {
  __proto__: animal,
  name: "Rabbit",
  eat() {         // rabbit.eat.[[HomeObject]] == rabbit
    super.eat();
  }
};

let longEar = {
  __proto__: rabbit,
  name: "Long Ear",
  eat() {         // longEar.eat.[[HomeObject]] == longEar
    super.eat();
  }
};

// يعمل التابِع كما نريد
longEar.eat();  // Long Ear eats.

عملت الشيفرة كما المفترض ذلك بسبب آلية عمل [[HomeObject]]. تعرف التوابِع (مثل longEar.eat) خاصيةَ [[HomeObject]] لها وتأخذ التابِع الأبّ من كائن prototype لذاك الكائن، ودون استعمال this أبدًا.

التوابِع ليست ”حرّة“

كما نعلم فالتوابع -بنحوٍ عام- ”حرّة“ وليست مروبطة بأيّ كائن. فيمكننا نسخها بين الكائنات واستعمالها بتمرير قيمة this أخرى.

ولكن وجود [[HomeObject]] يخلّ بهذا المبدأ إذ تتذكّر التوابِع كائناتها الأصلية هكذا. يبقى هذا الارتباط وثيقًا للأبد إذ لا يمكننا تغيير [[HomeObject]].

ولكن المكان الوحيد الذي نستعمل فيه خاصية [[HomeObject]] (في لغة جافاسكربت) هو super. يعني أنّه لو لم يستعمل التابِع super فيمكننا عدّه حرًا ونمضي بنسخه وتوزيعه على الكائنات. ولكن متى استعملت super، ساءت الأمور.

إليك مثالًا كاملًا عن نتيجة خطأ بعد النسخ بسبب super:

let animal = {
  sayHi() {
    console.log(`I'm an animal`); // أنا حيوان
  }
};

// يرث صنف الأرنب صنفَ الحيوان
let rabbit = {
  __proto__: animal,
  sayHi() {
    super.sayHi();
  }
};

let plant = {
  sayHi() {
    console.log("I'm a plant"); // أنا نبات
  }
};

// يرث صنف الشجرة صنفَ النبات
let tree = {
  __proto__: plant,
  sayHi: rabbit.sayHi // (*)
};

tree.sayHi();  // أنا حيوان (؟!؟)

باستدعاء tree.sayHi()‎ نرى الشجرة تقول ”أنا حيوان“. لا، لا! سبب ذلك بسيط جدًا:

  • في السطر (*) نسخنا التابِع tree.sayHi من rabbit. من يدري، ربّما لنقلّل من تكرار الشيفرات؟
  • وخاصية [[HomeObject]] لها هي الصنف rabbit، إذ صنعنا التابِع داخل rabbit، وما من طريقة لتغيير [[HomeObject]].
  • نرى في شيفرة التابِع tree.sayHi()‎ الاستدعاءَ super.sayHi()‎، وهو ينتقل إلى أعلى عند rabbit ويأخذ التابِع من animal.

إليك صورة توضّح ما يحدث:

super-homeobject-wrong.png

توابِع لا صفات داليّة

تُعرّف اللغة عن خاصيات [[HomeObject]] للتوابِع في الأصناف وفي الكائنات العادية. ولكن في حالة الكائنات فيجب تعريف التوابِع هكذا تمامًا method()‎ وليس هكذا "method: function()‎".

قد لا نرى فرقًا جوهريًا في الطريقتين، لكنّ محرّكات جافاسكربت تراه كذلك.

استعملنا في المثال أسفله صياغة ليست بتابِع للموازنة. بهذا لم تُضبط خاصية [[HomeObject]] ولن تعمل الوراثة:

let animal = {
  eat: function() { // ‫نكتبها هكذا بدل eat()‎ عمدًا {...
    // ...
  }
};

let rabbit = {
  __proto__: animal,
  eat: function() {
    super.eat();
  }
};

rabbit.eat();  // Error calling super ‫(إذ [[HomeObject]] غير موجودة)

خلاصة‎

  1. نستعمل class Child extends Parent لتوسعة الأصناف.
    • أي أنّ خاصية Child.prototype.__proto__‎ ستكون Parent.prototype، فتصير التوابِع موروثة.
  2. عندما نعيد تعريف الباني:
    • علينا استدعاء الباني الأبّ باستعمال super()‎ في الباني ”الابن“ ذلك قبل استعمال this.
  3. عندما نعيد تعريف أي تابع آخر:
    • يمكننا استعمال super.method()‎ في التابِع ”الابن“ لاستدعاء التابِع ”الأبّ“.
  4. أمور داخلية:
    • تتذكّر التوابِع صنفها/كائنها وتحفظه في خاصية [[HomeObject]] الداخلية، هكذا يحلّ super التوابِع الأب.
    • بذلك يكون ليس من الآمن نسخ تابِع لديه super من كائن ووضعه في آخر.

كما وأنّ:

  • ليس للدوال السهمية لا this ولا super

تمارين

خطأ في إنشاء سيرورة

الأهمية: 5

إليك الشيفرة التي نُوسّع فيها صنف Rabbit من صنف Animal.

للأسف فلا يمكننا صناعة كائنات الأرانب. ما المشكلة؟ أصلِحها في طريقك.

class Animal {

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

}

class Rabbit extends Animal {
  constructor(name) {  
    this.name = name;
    this.created = Date.now();
  }
}

// هنا
let rabbit = new Rabbit("White Rabbit"); // Error: this is not defined
alert(rabbit.name);

الحل

هذا لأنّ على الباني الابن استدعاء super()‎.

إليك الشيفرة الصحيحة:

class Animal {

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

}

class Rabbit extends Animal {
  constructor(name) {  
    super(name); // هنا
    this.created = Date.now();
  }
}

let rabbit = new Rabbit("White Rabbit"); // الآن تمام
alert(rabbit.name); // White Rabbit

توسعة ساعة

الأهمية: 5_

لدينا صنف ساعة Clock، وهو حاليًا يطبع الوقت في كلّ ثانية.

class Clock {
  constructor({ template }) {
    this.template = template;
  }

  render() {
    let date = new Date();

    let hours = date.getHours();
    if (hours < 10) hours = '0' + hours;

    let mins = date.getMinutes();
    if (mins < 10) mins = '0' + mins;

    let secs = date.getSeconds();
    if (secs < 10) secs = '0' + secs;

    let output = this.template
      .replace('h', hours)
      .replace('m', mins)
      .replace('s', secs);

    console.log(output);
  }

  stop() {
    clearInterval(this.timer);
  }

  start() {
    this.render();
    this.timer = setInterval(() => this.render(), 1000);
  }
}

أنشِئ الصنف الجديد ExtendedClock ليرث من Clock وأضِف المُعامل precision، وهو عدد الملّي ثانية ms بين كلّ ”تَكّة“. يجب أن يكون مبدئيًا 1000 (أي ثانية كاملة).

  • ضع شيفرتك في الملف extended-clock.js.
  • تعديل ملف clock.js الأصلي ممنوع. وسّع الصنف.

يمكن الاعتماد على هذه البيئة التجريبية لحل التمرين.

الحل

class ExtendedClock extends Clock {
  constructor(options) {
    super(options);
    let { precision = 1000 } = options;
    this.precision = precision;
  }

  start() {
    this.render();
    this.timer = setInterval(() => this.render(), this.precision);
  }
};

مشاهدة الحل في بيئة تجريبية.

الأصناف تُوسّع الكائنات؟

الأهمية: 5

كما نعلم فالكائنات كلها ترث Object.prototype وتقدر على الوصول إلى توابِع الكائنات ”العادية“ مثل hasOwnProperty وغيرها.

مثال سريع:

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

let rabbit = new Rabbit("Rab");

// ‫التابِع hasOwnProperty مأخوذ من Object.prototype
alert( rabbit.hasOwnProperty('name') ); // true

ولكن لو كتبنا ذلك جهارةً هكذا "class Rabbit extends Object" فالناتج يختلف عن "class Rabbit" فقط! غريب.

تُراه ما الفرق؟

إليك مثالًا عمّا أقصد (الشيفرة لا تعمل، لماذا؟ أصلِحها!):

class Rabbit extends Object {
  constructor(name) {
    this.name = name;
  }
}

let rabbit = new Rabbit("Rab");

alert( rabbit.hasOwnProperty('name') ); // true

الحل

أولًا نرى ما مشكلة الشيفرة تلك.

متى شغّلنا الشيفرة بان سبب المشكلة: على باني الأصناف الموروثة استدعاء super()‎ وإلّا تكون قيمة "this" ”غير معرّفة“.

لذا سنصلحها:

class Rabbit extends Object {
  constructor(name) {
    super(); // علينا استدعاء الباني الأب حين نرث الصنف
    this.name = name;
  }
}

let rabbit = new Rabbit("Rab");

alert( rabbit.hasOwnProperty('name') ); // true

ولكن… لم ننتهِ بعد.

حتّى مع هذا … فهناك فرق مهم جوهري بين "class Rabbit extends Object" وclass Rabbit.

كما نعلم فصياغة extends تضبط كائنا prototype:

  1. بين توابِع الباني لـِ"prototype" (بالنسبة للتوابع العادية).
  2. بين توابِع الباني نفسها (بالنسبة للتوابع الثابتة).

في حالتنا إن class Rabbit extends Object تعني:

class Rabbit extends Object {}

alert( Rabbit.prototype.__proto__ === Object.prototype ); // (1) true
alert( Rabbit.__proto__ === Object ); // (2) true

لذا يوفر Rabbit إمكانية الوصول إلى الدوال الثابتة للكائن Object. هكذا:

class Rabbit extends Object {}

*!*
// عادةً نستدعي ‫Object.getOwnPropertyNames
alert ( Rabbit.getOwnPropertyNames({a: 1, b: 2})); // a,b
*/!*

ولكن إذا لم يكن لدينا extends Object فلن تُسند Rabbit.__proto__‎ للصنف Object.

إليك المثال:

class Rabbit {}

alert( Rabbit.prototype.__proto__ === Object.prototype ); // (1) true
alert( Rabbit.__proto__ === Object ); // (2) false (!)
alert( Rabbit.__proto__ === Function.prototype ); // as any function by default

*!*
// error, no such function in Rabbit
alert ( Rabbit.getOwnPropertyNames({a: 1, b: 2})); // Error
*/!*

في هذه الحالة إن Rabbit لن يزودنا بطريقة للوصول إلى التوابع الثابتة في Object.

بالمناسبة يملك النموذج الأولي للتوابع Function.prototype دوال مُعمَّمة مثل :call و bind ..إلخ. وهي متاحة دائمًا في كِلا الحالتين، لأن باني Object المضمن في اللغة هو Object.__proto__ === Function.prototype.

rabbit-extends-object.png

لذلك وباختصار هناك اختلافان وهما:

class Rabbit class Rabbit extends Object
-- يحتاج لاستدعاءsuper()‎ في الباني
Rabbit.__proto__ === Function.prototype Rabbit.__proto__ === Object

ترجمة -وبتصرف- للفصل Class inheritance من كتاب The JavaScript language





تفاعل الأعضاء


لا توجد أيّة تعليقات بعد



يجب أن تكون عضوًا لدينا لتتمكّن من التعليق

انشاء حساب جديد

يستغرق التسجيل بضع ثوان فقط


سجّل حسابًا جديدًا

تسجيل الدخول

تملك حسابا مسجّلا بالفعل؟


سجّل دخولك الآن