يشير مصطلح prototype إلى اﻵلية التي ترث فيها الكائنات ميزات من بعضها في جافا سكريبت، ويختلف عملها عن الوراثة في غيرها من اللغات كائنية التوجه، وهذا ما سنشرحه في هذا المقال.
ننصحك قبل أن تبدأ العمل معنا في هذه السلسلة أن تطلع على:
- أساسيات HTML.
- أساسيات عمل CSS
- أساسيات جافا سكريبت
- أساسيات البرمجة كائنية التوجه في جافا سكربت كما شرحناه في مقال أساسيات العمل مع الكائنات في جافا سكريبت.
سلسلة من اﻷنماط المجرّدة
حاول أن تنشئ في طرفية جافا سكريبت في متصفحك الكائن التالي:
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.
لكن لا يمثل 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
.
وعندما تستدعي توابع مثل ()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
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.