اذهب إلى المحتوى

استخدام كائنات Prototype في جافا سكريبت


ابراهيم الخضور

يشير مصطلح prototype إلى اﻵلية التي ترث فيها الكائنات ميزات من بعضها في جافا سكريبت، ويختلف عملها عن الوراثة في غيرها من اللغات كائنية التوجه، وهذا ما سنشرحه في هذا المقال.

ننصحك قبل أن تبدأ العمل معنا في هذه السلسلة أن تطلع على:

  1. أساسيات HTML.
  2. أساسيات عمل CSS
  3. أساسيات جافا سكريبت
  4. أساسيات البرمجة كائنية التوجه في جافا سكربت كما شرحناه في مقال أساسيات العمل مع الكائنات في جافا سكريبت.

سلسلة من اﻷنماط المجرّدة

حاول أن تنشئ في طرفية جافا سكريبت في متصفحك الكائن التالي:

const myObject = {
  city: "Madrid",
  greet() {
    console.log(`Greetings from ${this.city}`);
  },
};

myObject.greet(); // Greetings from Madrid

يمتلك الكائن خاصية واحدة لتخزين البيانات هي city وتابعًا واحدًا هو ()greet. فإن كتبت اسم الكائن تليه نقطة في الطرفية مثل .myobject، ستعرض الطرفية قائمة بجميع الخاصيات التي يمتلكها العنصر. وسترى إضافة إلى الخاصية city والتابع ()greet، الكثير من الخاصيات اﻷخرى!

__defineGetter__
__defineSetter__
__lookupGetter__
__lookupSetter__
__proto__
city
constructor
greet
hasOwnProperty
isPrototypeOf
propertyIsEnumerable
toLocaleString
toString
valueOf

جرّب الوصول إلى إحداها:

myObject.toString(); // "[object Object]"

لقد نجح اﻷمر (حتى لو لم يكن واضحًا لك بالضبط ما الذي يفعله التابع ()toString هنا). فما قصة هذه الخاصيات اﻹضافية، ومن أين أتت؟

في الواقع يملك كل كائن في جافا سكريبت خاصية مضمنة تُدعى prototype وهي بحد ذاتها كائن أيضًا ويضم بدوره خاصية أو كائن مجرّد إن صح التعبير، مما يوّلد ما يُدعى سلسلة prototype chain. تنتهي السلسلة عند الوصول إلى كائن قيمة الخاصية prototype له تساوي null.

ملاحظة:لا تُدعى الخاصية التي تشير إلى prototype بالاسم prototype، إذ ليس لها اسم معياري لكنها تُكتب بالممارسة العملية بالشكل _proto_. وتُعد الطريقة المعيارية للوصول إلى الخاصية prototype هي استخدام التابع ()Object.getPrototypeOf.

عندما تحاول الوصول إلى إحدى خاصيات كائن، ولم يُعثر على الخاصية في الكائن نفسه، يجري البحث عنها ضمن الكائن prototype، وإن لم يُعثر عليها يجري البحث مجددًا ضمن الكائن prototype للكائن prototype حتى نهاية السلسلة، فإن لم يجدها، سيُعيد القيمة undefined.

فعندما تُنفّذ التعليمة ()myObject.toString:

  • يبحث المتصفح عن التابع toString ضمن الكائن myObject.
  • إن لم يجده، سيبحث عنه في الكائن prototype للكائن myObject.
  • يجده هناك ويستدعيه.

لكن ما هو prototype للكائن myObject؟ لمعرفة ذلك، يمكننا استخدام الأمر:

Object.getPrototypeOf(myObject); // Object { }

سنجد أن prototype هو كائن يُدعى Object.prototype، وهو أبسط الكائنات prototype، وتمتلكه جميع الكائنات افتراضيًا، والكائن prototype الخاص به هو null. لذا يقع هذا الكائن في نهاية سلسلة كائنات prototype.

01 myobject prototype chain

لكن لا يمثل Object.prototype دائمًا prototype لكل كائن، جرّب ما يلي لترى:

const myDate = new Date();
let object = myDate;

do {
  object = Object.getPrototypeOf(object);
  console.log(object);
} while (object);

// Date.prototype
// Object { }
// null

تنشئ الشيفرة السابقة كائن من النوع Date، ثم تنتقل ضمن سلسلة كائنات prototype الخاصة به وتسجل أسماء هذه الكائنات. وتظهر أن النوع المجرد للكائن myDate هو Date.prototype وprototype الخاص بهذا اﻷخير هو Object.prototype.

02 mydate prototype chain

وعندما تستدعي توابع مثل ()mydate2.getMonth، فأنت تستدعي في واقع اﻷمر توابع معرّفة ضمن النوع Date.prototype.

إخفاء الخاصيات

ما الذي يحدث إن عرّفت خاصية في كائن وكانت هناك خاصية معرّفة بنفس الاسم ضمن الكائن prototype له؟ ألق نظرة على الشيفرة التالية:

const myDate = new Date(1995, 11, 17);

console.log(myDate.getYear()); // 95

myDate.getYear = function () {
  console.log("something else!");
};

myDate.getYear(); // 'something else!'

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

إعداد كائنات prototype

هناك طرق مختلفة ﻹعداد وضبط هذه الكائنات في جافا سكريبت، و سنناقش هنا طريقتان: الأولى باستخدام ()Object.create والثانية استخدام الدوال البانية constructors.

استخدام التابع ()Object.create

يُنشئ التابع ()Object.create كائنًا جديدًا ويسمح لك بتخصيص كائن ليصبح prototype الجديد الخاص به، إليك مثالًا:

const personPrototype = {
  greet() {
    console.log("hello!");
  },
};

const carl = Object.create(personPrototype);
carl.greet(); // hello!

أنشأنا في الشيفرة السابقة كائنًا باسم personPrototype، يمتلك التابع ()greet، ثم أنشأنا كائنًا جديدًا باستخدام التابع ()Object.create وجعلنا personPrototype كائن prototype له. وبالتالي نستطيع اﻵن استدعاء التابع ()greet من خلال الكائن الجديد، لأن كائن prototype قد زوّده به.

استخدام الدالة البانية

تمتلك جميع الدوال في جافا سكريبت خاصية تًدعى prototype. وعندما تستدعي الدالة على شكل دالة بانية، تُضبط تلك الخاصية لتكون prototype للكائن المبني حديثًا (داخل الخاصية التي تُدعى _proto_ تقليديًا).

لهذا، وعندما نضبط القيمة prototype للدالة البانية، نضمن أن الكائنات التي تُنشئها هذه الدالة تمتلك كائن prototype:

const personPrototype = {
  greet() {
    console.log(`hello, my name is ${this.name}!`);
  },
};

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

Object.assign(Person.prototype, personPrototype);
// or
// Person.prototype.greet = personPrototype.greet;

لقد أنشأنا هنا:

  • كائنًا بالاسم personPrototype يمتلك التابع ()greet.
  • دالة بانية ()Person تهيئ اسم الشخص الذي نحييه.

وضعنا بعد ذلك التوابع المعرّفة ضمن الكائن personPrototype ضمن الكائن prototype للدالة البانية باستخدام التابع Object.assign. وهكذا ستمتلك الكائنات المبنية باستخدام الدالة ()Person prototype Person.prototype الذي يضم تلقائيًا التابع greet.

const reuben = new Person("Reuben");
reuben.greet(); // hello, my name is Reuben!

يشرح هذا أيضًا ما قلناه سابقًا بأن الكائن prototype للكائن myDate هو Date.prototype، إذ يمثّل الخاصية للبانية Date.

الخاصيات المملوكة Own properties

يمتلك الكائن الذي أنشأناه باستخدام البانية ()Person خاصيتين:

  • الخاصية name التي ضبطنا قيمتها باستخدام الدالة البانية، لهذا تظهر مباشرة ضم الكائن Person.
  • التابع ()greetالذي ضبُط من خلال الكائن prototype.

من الشائع أن تشاهد هذا الأسلوب الذي تُعرّف فيه التوابع ضمن كائنات prototype، وتُعرّف فيه خاصيات البيانات ضمن الدوال البانية. ذلك أن التوابع تبقى نفسها عادة لجميع الكائنات التي ننشئها، لكننا غالبا ما نريد أن يأخذ كل كائن قيم مخصصة لخاصيات البيانات (كأن يكون لكل شخص اسم خاص).

تُدعى الخاصيات التي تُعرّف مباشرة ضمن الكائن مثل الخاصية name بالخاصيات المملوكة Own Property، وبإمكانك التحقق من كون الخاصية مملوكة باستخدام التابع الساكن ()Object.hasOwn:

const irma = new Person("Irma");

console.log(Object.hasOwn(irma, "name")); // true
console.log(Object.hasOwn(irma, "greet")); // false

ملاحظة: كما تستطيع استخدام التابع غير الساكن ()Object.hasOwnProperty في هذه الحالة لكننا ننصح باستخدام التابع الساكن ما أمكن.

الكائنات prototype والوراثة

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

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

أما في جافا سكريبت فيمكن للكائنين Professor و Student أن يمتلكا نفس الكائن المجرد Person، ويرثا الخاصيات التي يمتلكها كائن prototype كما يمكن أن نعرّف خاصيات وتوابع جديدة تناسب كل منهما.

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

الخلاصة

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

ترجمة -وبتصرف- للمقال Object prototpes

اقرأ أيضًا


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

أفضل التعليقات

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



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...