المسارات
/
JavaScript
JavaScript
/
المنهج
/
النماذج الأولية والأصناف
ال

النماذج الأولية والأصناف في JavaScript

32 تمرينًا

نبذة عن النماذج الأولية والأصناف

تتضمّن JavaScript إمكانات البرمجة كائنية التوجه (OOP). في البرمجة كائنية التوجه، تريد إنشاء كائنات (نسخ) من «قوالب» (أصناف) بحيث تتضمّن بيانات ووظائف معيّنة. تُسمّى خصائص البيانات حقولًا في سياق البرمجة كائنية التوجه، وتُسمّى خصائص الدوال طرقًا.

لم تكن JavaScript تمتلك أصنافًا على الإطلاق قبل إضافتها إلى مواصفات اللغة في 2015، لكنها كانت تتيح البرمجة كائنية التوجه باستخدام الوراثة القائمة على النموذج الأولي. وعلى الرغم من توفّر الكلمة المفتاحية class في أيامنا هذه، فإن JavaScript لا تزال لغة قائمة على النموذج الأولي.

لفهم ما يعنيه أن تكون اللغة قائمة على النموذج الأولي، وكيف تعمل JavaScript فعليًا، سنعود إلى زمن لم تكن فيه أصناف.

صيغة النموذج الأولي

دالة الإنشاء

في JavaScript، يتحقّق القالب (الصنف) عبر دالة عادية. وعندما يُفترض استخدام دالة كقالب من هذا النوع، فإنها تُسمّى دالة إنشاء، والعُرف أن يبدأ اسم الدالة بحرف كبير. وتُشتقّ النسخ (الكائنات) من القالب باستخدام الكلمة المفتاحية new عند استدعاء دالة الإنشاء.

function Car() {
  // ...
}

const myCar = new Car();
const yourCar = new Car();

من المهم ملاحظة أن النسخ ودالة الإنشاء تحتفظ بعلاقة تربطها ببعضها حتى بعد إنشاء النسخ. يتضمّن كل كائن نسخة خاصية داخلية مخفية يُشار إليها باسم [[prototype]] في مواصفات اللغة. وهي تحمل مرجعًا إلى قيمة المفتاح prototype في دالة الإنشاء. نعم، لقد قرأت ذلك صحيحًا: يمكن لدالة في JavaScript أن تمتلك أزواجًا من المفتاح والقيمة لأنها أيضًا كائن تحت الغطاء.

منذ عام 2015، يمكن الوصول إلى [[prototype]] عبر Object.getPrototypeOf(). وقبل ذلك، كان الوصول إليه ممكنًا عبر المفتاح __proto__ في كثير من البيئات.

لا تخلط بين النموذج الأولي للكائن ([[prototype]]) وخاصية prototype في دالة الإنشاء.

Note

للتلخيص:

  • دوال الإنشاء في JavaScript دوال عادية.
  • إنشاء نسخة جديدة يُنتج كائنًا له علاقة بدالة إنشائه تُسمّى النموذج الأولي الخاص به.
  • الدوال كائنات (كائنات قابلة للاستدعاء)، ولذلك يمكن أن تمتلك خصائص.
  • خاصية prototype في دالة الإنشاء ستصبح النموذج الأولي للنسخة.

حقول النسخ

غالبًا ما تريد أن تتضمّن كل الكائنات المشتقّة (النسخ) بعض الحقول، وأن تمرّر لها بعض القيم الأولية عند إنشاء الكائن. ويمكن تحقيق ذلك عبر الكلمة المفتاحية this. داخل دالة الإنشاء، يمثّل this الكائن الجديد الذي سيُنشأ عبر new. ويُرجَع this تلقائيًا من دالة الإنشاء عند استدعائها باستخدام new.

وهذا يعني أنه يمكننا إضافة حقول إلى النسخة الجديدة بإضافتها إلى this داخل دالة الإنشاء.

function Car(color, weight) {
  this.color = color;
  this.weight = weight;
  this.engineRunning = false;
}

const myCar = new Car('red', '2mt');
myCar.color;
// => 'red'
myCar.engineRunning;
// => false

طرق النسخ

تُضاف الطرق عبر خاصية prototype في دالة الإنشاء. وداخل الطريقة، يمكنك الوصول إلى حقول النسخة عبر this. وهذا يعمل بفضل القاعدة العامة التالية.

عند استدعاء دالة كطريقة لكائن، تُضبط قيمة this فيها على الكائن الذي استُدعيت عليه الطريقة. 1

function Car() {
  this.engineRunning = false;
  // ...
}

Car.prototype.startEngine = function () {
  this.engineRunning = true;
};

Car.prototype.addGas = function (litre) {
  // ...
};

const myCar = new Car();
myCar.startEngine();
myCar.engineRunning;
// => true

سلسلة النموذج الأولي

myCar في المثال أعلاه كائن عادي في JavaScript، ولو أردنا فحصه (مثلًا في وحدة تحكم المتصفح)، فلن نجد داخل كائن myCar مباشرة خاصية startEngine قيمتها دالة. فكيف يعمل الكود أعلاه إذن؟

السر هنا يُسمّى سلسلة النموذج الأولي. عندما تحاول الوصول إلى أي خاصية (حقل أو طريقة) لكائن ما، تتحقق JavaScript أولًا مما إذا كان المفتاح المقابل موجودًا مباشرة في الكائن نفسه. وإن لم يكن موجودًا، فإنها تواصل البحث عن المفتاح في الكائن الذي تشير إليه خاصية [[prototype]] في الكائن الأصلي. وكما ذُكر سابقًا، تشير [[prototype]] في مثالنا إلى خاصية prototype في دالة الإنشاء. وهناك ستجد JavaScript دالة startEngine لأننا أضفناها هناك.

function Car() {
  // ...
}

Car.prototype.startEngine = function () {
  // ...
};

ولا تنتهي سلسلة النموذج الأولي عند هذا الحد. تشير خاصية [[prototype]] في Car.prototype (myCar.[[prototype]].[[prototype]]) إلى Object.prototype (خاصية prototype في دالة إنشاء Object). وهي تتضمّن طرقًا عامة متاحة لجميع كائنات JavaScript، مثل toString(). عادةً ما تكون قيمة [[prototype]] في Object مساوية لـ null، وهناك تنتهي سلسلة النموذج الأولي. وخلاصة القول، يمكنك استدعاء myCar.toString() وستكون هذه الطريقة موجودة لأن JavaScript تبحث عن هذه الطريقة في سلسلة النموذج الأولي بأكملها. ويمكنك العثور على مثال مفصّل في مقالة MDN بعنوان «الوراثة وسلسلة النموذج الأولي».

Caution

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

الطرق الديناميكية (إضافة طرق إلى جميع النسخ الموجودة)

تتيح لك JavaScript إضافة طرق إلى جميع النسخ الموجودة حتى بعد إنشائها.

تعلّمنا أن كل نسخة تحتفظ بمرجع إلى خاصية prototype في دالة الإنشاء. وهذا يعني أنك إذا أضفت مدخلًا إلى كائن prototype ذلك، فإن هذا المدخل الجديد (مثل طريقة جديدة) يصبح متاحًا فورًا لجميع النسخ المنشأة من دالة الإنشاء تلك.

function Car() {
  this.engineRunning = false;
}

const myCar = new Car();
// Calling myCar.startEngine() here would result in "TypeError:
// myCar.startEngine is not a function".

Car.prototype.startEngine = function () {
  this.engineRunning = true;
};

myCar.startEngine();
// This works, even though myCar was created before the method
// was added.

من الناحية النظرية، يمكن استخدام الطرق الديناميكية حتى لتوسيع وظائف الكائنات المضمّنة مثل Object أو Array بتعديل نموذجها الأولي. ويُسمّى هذا monkey patching. ولأن هذا التغيير يؤثر على التطبيق بأكمله، ينبغي تجنّبه لمنع الآثار الجانبية غير المقصودة. والاستخدام المعقول الوحيد هو توفير polyfill لطريقة مفقودة في بيئات أقدم.

صيغة الصنف

في أيامنا هذه، تدعم JavaScript تعريف الأصناف بالكلمة المفتاحية class. وقد أُضيف ذلك إلى مواصفات اللغة في 2015. من جهة، يوفّر هذا سكرًا نحويًا يجعل الأصناف أسهل في القراءة والكتابة. الصيغة الجديدة أقرب إلى أسلوب كتابة الأصناف في لغات مثل C++ أو Java. والمطوّرون المنتقلون من تلك اللغات يجدون تكيّفًا أسهل. ومن جهة أخرى، تمهّد صيغة الصنف الطريق لميزات لغوية جديدة غير متاحة في صيغة النموذج الأولي.

إعلانات الأصناف

بالصيغة الجديدة، تُعرَّف الأصناف بالكلمة المفتاحية class، يتبعها اسم الصنف وجسم الصنف داخل الأقواس المعقوفة. ويحتوي الجسم على تعريف دالة الإنشاء، أي طريقة خاصة باسم constructor. وتعمل هذه الدالة تمامًا مثل دالة الإنشاء في صيغة النموذج الأولي. كما يحتوي جسم الصنف على جميع تعريفات الطرق. وصيغة الطرق شبيهة بالصيغة المختصرة التي رأيناها لإضافة الدوال كقيم داخل كائن، انظر مفهوم الكائنات.

class Car {
  constructor(color, weight) {
    this.color = color;
    this.weight = weight;
    this.engineRunning = false;
  }

  startEngine() {
    this.engineRunning = true;
  }

  addGas(litre) {
    // ...
  }
}

const myCar = new Car();
myCar.startEngine();
myCar.engineRunning;
// => true

على غرار إعلانات الدوال وتعبيرات الدوال، تدعم JavaScript أيضًا تعبيرات الأصناف بالإضافة إلى إعلان الصنف الموضّح أعلاه.

ضع في اعتبارك أنه تحت الغطاء، لا تزال JavaScript لغة قائمة على النموذج الأولي. ولا تزال جميع الآليات التي تعلّمناها في قسم «صيغة النموذج الأولي» أعلاه سارية.

الحقول الخاصة والدوال الجالبة والضابطة

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

لا تزال إضافة الحقول الخاصة الفعلية إلى مواصفات اللغة قيد الإنجاز، انظر وثيقة المقترح للتفاصيل.

وفي غضون ذلك، يمكنك الاستفادة من العُرف المستقر بأن الحقول والطرق التي تبدأ بشرطة سفلية ينبغي معاملتها كخاصة. وينبغي ألا يُوصَل إليها مباشرةً من خارج الصنف.

ترافق الحقول الخاصة أحيانًا دوال جالبة ودوال ضابطة. بالكلمتين المفتاحيتين get وset يمكنك تعريف دوال تُنفّذ عند الوصول إلى خاصية تحمل اسم الدالة نفسه أو عند إسناد قيمة إليها.

class Car {
  constructor() {
    this._mileage = 0;
  }

  get mileage() {
    return this._mileage;
  }

  set mileage(value) {
    throw new Error(`Mileage cannot be manipulated, ${value} is ignored.`);
    // Just an example, usually you would not provide a setter in this case.
  }
}

const myCar = new Car();
myCar.mileage;
// => 0
myCar.mileage = 100;
// => Error: Mileage cannot be manipulated, 100 is ignored.

حقول الصنف وطرق الصنف

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

class Car {
  static type = 'vehicle';

  static isType(targetType) {
    return targetType === 'vehicle';
  }
}

Car.type;
// => 'vehicle'

Car.isType('road sign');
// => false

الوراثة القائمة على الأصناف

إلى جانب نوع الوراثة عبر سلسلة النموذج الأولي الذي رأيناه سابقًا، يمكنك أيضًا تمثيل الوراثة بين الأصناف في JavaScript. وهذا مشروح في مفهوم الوراثة.


  1. أمثلة this - كطريقة لكائن، MDN. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this#as_an_object_method (بالرجوع إليه في 3 ديسمبر 2021) ↩

تعديل عبر GitHub يفتح الرابط في نافذة أو علامة تبويب جديدة

تعلّم النماذج الأولية والأصناف