سنكمل في هذا الدرس الحديث عن موضوع الوراثة النموذجية الذي بدأناه في الدرس السابق.
لا تنسَ بأنّك يمكنك إنشاء كائنات جديدة من خلال دالّة الباني (مثل new F() ). لو كان F.prototype كائن جافاسكربت، فإن المعامِل new سيضبط الخاصية [[Prototype]] لهذا الكائن الجديد.
من بداية تضمين لغة جافاسكربت للوراثة النموذجية جعلتها من المميزات الأساسية في اللغة. ولكن في الماضي لم يكن هنالك القدرة للوصول المباشر للوراثة النموذجية والطريقة الوحيدة الّتي حلّت محلها هي خاصية "prototype" في دالّة الباني. سنشرح في هذا الدرس كيفية استخدامها لأنه مازال العديد من الشيفرات البرمجية القديمة تستخدمها.
لاحظ بأنّ F.prototype هنا تعني وجود خاصية عادية باسم "prototype" للكائن F. ربما تفكّر وكأنها النموذج الأولي لهذا الكائن، ولكن لا… فهنا نعني حرفيًا أنها خاصية عادية لها هذا الاسم.
إليك مثالًا:
let animal = { eats: true }; function Rabbit(name) { this.name = name; } Rabbit.prototype = animal; // هنا let rabbit = new Rabbit("White Rabbit"); // rabbit.__proto__ == animal alert( rabbit.eats ); // true
تعني التعليمة Rabbit.prototype = animal حرفيًا الآتي: "ما إن يُنشأ كائن new Rabbit، أسنِد خاصية [[Prototype]] له لتكون للكائن animal".
إليك الصورة الناتجة:
في الصورة نرى "prototype" في سهم أفقي (أي أنّها خاصية عادية) بينما [[Prototype]] في سهم رأسي (أي أنّها توضّح وراثة كائن rabbit للكائن animal).
إن الخاصية F.prototype تستخدم عند الإنشاء فقط أي عندما تستدعى تعليمة new F وتُسند للكائن القيمة المناسبة للخاصية [[Prototype]]. في حال تغيرت الخاصية F.prototype مثلًا (F.prototype = <another object>)، عندها ستحصل الكائنات المنشأة بعد هذا التغيير على القيمة الجديدة للخاصية [[Prototype]] (أي الكائن الجديد)، ولكن الكائنات القديمة مازالت تحتفظ بالقيمة القديمة.
القيمة الإفتراضية للخاصية prototype في الباني
لكلّ دالة خاصية "prototype" حتّى لو لم نقدّمها نحن.
إن القيمة الإفتراضية للخاصية "prototype" تُشير إلى نفس الدالّة.
هكذا تمامًا:
function Rabbit() {} /* كائن prototype Rabbit.prototype = { constructor: Rabbit }; */
يمكننا فحص ذلك أيضًا:
function Rabbit() {} // مبدئيًا: // Rabbit.prototype = { constructor: Rabbit } alert( Rabbit.prototype.constructor == Rabbit ); // true
طبيعيًا، إن لم نعدل أي شيء، ستكون خاصية constructor مُتاحة لكلّ كائنات rabbit من خلال كائن [[Prototype]]:
function Rabbit() {} // مبدئيًا: // Rabbit.prototype = { constructor: Rabbit } let rabbit = new Rabbit(); // ترث من {constructor: Rabbit} alert(rabbit.constructor == Rabbit); // true من prototype
يمكننا استعمال الخاصية constructor لإنشاء كائن جديد باستعمال نفس الباني الّذي أنشأ الكائن الموجود حاليًا.
هكذا:
function Rabbit(name) { this.name = name; alert(name); } let rabbit = new Rabbit("White Rabbit"); // انظر let rabbit2 = new rabbit.constructor("Black Rabbit");
يُفيدنا هذا حين نكون أمام كائن ولكن لا نعرف الباني الحقيقي الّذي بناه (ربما أتى من مكتبة خارجية)، وأردنا إنشاء كائن آخر مثله.
ولكن الأمر الأهم الّذي يتعلّق بِـ "constructor" هو أنّ لغة جافاسكربت نفسها لا تتأكّد من صحّة قيمة خاصية "constructor".
نعم كما قرأت، الخاصية موجودة في "prototype" للدوالّ، وهذا كلّ ما في الأمر. إذ ستعتمد لغة جافاسكربت علينا فيما سيحدث لاحقًا.
فمثلًا لو أردنا استبدال القيمة الإفتراضية للخاصية prototype، فلن يملك الكائن أيّ خاصية "constructor".
مثال:
function Rabbit() {} Rabbit.prototype = { jumps: true }; let rabbit = new Rabbit(); // لاحظ alert(rabbit.constructor === Rabbit); // false
ولهذا لنُبقي على خاصية "constructor" الصحيحة يمكننا إضافة الخاصيات وإزالتها من كائن "prototype" الإفتراضي بدل الطريقة السابقة. هكذا:
function Rabbit() {} // بدل الكتابة على كلّ Rabbit.prototype // نُضيف ما نريد إليه Rabbit.prototype.jumps = true // هكذا تبقى خاصية Rabbit.prototype.constructor الإفتراضية محفوظة
أو يمكننا (لو أردنا) إعادة إنشاء الخاصية constructor يدويًا:
Rabbit.prototype = { jumps: true, constructor: Rabbit // هنا }; // الآن سيكون المُنشِئ صحيحًا إذ أنّا من أضفناه
خلاصة
شرحنا في هذا الفصل سريعًا طريقة ضبط كائن [[Prototype]] للكائنات الّتي أنشأتها بدالّة الباني. سنرى لاحقًا أنماط متقدّمة في البرمجة تعتمد على هذا الطريقة.
ما أخذناه بسيط، ولكن بعض الأمور نوضّحها ثانيةً للتأكّد:
-
تضبط الخاصية
F.prototype(لا تظنّها كائن[[Prototype]]) لكائنٍ ما الخاصية[[Prototype]]لكلّ الكائنات الجديدة متى استدعيتnew F(). -
يجب أن تكون قيمة
F.prototypeإمّا كائنًا أوnull، ولن تعمل أيّة قيم أخرى. -
هذا التأثير للخاصية
"prototype"موجود فقط حين يُضبط في دالة الباني وحين يُنفّذ بتعليمةnew.
في الكائنات العادية ليست بخاصية خاصة جدًا:
let user = { name: "John", prototype: "Bla-bla" // نزعنا السحر };
لكلّ الدوالّ مبدئيًا F.prototype = { constructor: F }، فيمكننا أن نأخذ باني معين من كائن ما بالدخول إلى الخاصية "constructor" الخاصة به.
تمارين
تغيير الخاصية ”prototype“
الأهمية: 5
أنشأنا في الشيفرة أدناه كائنًا جديدًا new Rabbit وحاولنا بعدها تعديل الخاصية prototype لهذا الكائن.
بادئ ذي بدء، كانت الشيفرة:
function Rabbit() {} Rabbit.prototype = { eats: true }; let rabbit = new Rabbit(); alert( rabbit.eats ); // true
-
وأضفنا سلسلة نصية أخرى (عليها علامة). ماذا سيعرض التابِع
alert؟function Rabbit() {} Rabbit.prototype = { eats: true }; let rabbit = new Rabbit(); Rabbit.prototype = {}; // (*) alert( rabbit.eats ); // ?
-
وماذا لو… كانت الشيفرة كهذه (استبدلنا سطرًا فيها)؟
function Rabbit() {} Rabbit.prototype = { eats: true }; let rabbit = new Rabbit(); Rabbit.prototype.eats = false; // (*) alert( rabbit.eats ); // ?
-
وماذا عن هذه (استبدلنا سطرًا أيضًا)؟
function Rabbit() {} Rabbit.prototype = { eats: true }; let rabbit = new Rabbit(); delete rabbit.eats; // (*) alert( rabbit.eats ); // ?
-
وهذه… أيضًا:
function Rabbit() {} Rabbit.prototype = { eats: true }; let rabbit = new Rabbit(); delete Rabbit.prototype.eats; // (*) alert( rabbit.eats ); // ?
الحل
الإجابات:
-
true.عملية الإسناد على
Rabbit.prototypeتضع الخاصية[[Prototype]]للكائنات الجديدة، ولكنّها لا تعدّل على الكائنات الموجودة مسبقًا. -
false. عملية الإسناد تكون من خلال الخاصيةRabbit.prototype، إن الخاصية المشار إليها هناRabbit.prototypeليست مكررًا، وإنما بقيت يُشار إليها من خلالRabbit.prototypeو الخاصية[[Prototype]]للكائنrabbit.لذا حين نغيّر المحتوى في الطريقة الأولى سنرى النتائج في الطريقة الثانية.
-
true. كلّ عمليات الحذف تطبق مباشرة على الكائن. تحاول هذه التعليمةdelete rabbit.eatsحذف الخاصية المخصصة للكائنrabbitولكنها ليست لها. لذا العملية لن يكون لها أي تأثير. -
undefined.حُذفت الخاصية
eatsمن كائن prototype وما عادت موجودة بعد الآن.
إنشاء كائن جديد من خلال نفس باني لكائنٍ آخر
الأهمية: 5
تخيّل بأنّ لدينا الكائن الفريد obj وأنشأته بدالة الباني، ولكننا… لا نعرف أيّ دالة هذه، ولكن مع ذلك نريد استعمال نفس الباني لإنشاء كائن جديد آخر.
أيمكن لهذه الشيفرة إنجاز المهمة؟
let obj2 = new obj.constructor();
اكتب مثالين باستخدام بانيين للكائن obj، واحدًا يعمل مع الشيفرة أعلاه، وواحدًا لا يعمل له.
الحل
يمكن أن نستعمل هذه الطريقة لو كنّا متأكدين مئة بالمئة بأنّ خاصية "constructor" تحمل القيمة الصحيحة.
فمثلًا لو لم نعدّل على "prototype" المبدئية فستعمل هذه الشيفرة بلا ريب:
function User(name) { this.name = name; } let user = new User('John'); let user2 = new user.constructor('Pete'); alert( user2.name ); // Pete (عملت!)
نفذت الشيفرة تنفيذًا صحيحًا إذ أنّ User.prototype.constructor == User.
ولكن… لو أتى أحدهم مثلًا وكتب على User.prototype ونسي إعادة إنشاء constructor لتُشير إلى كائن المستخدم User، فلن تعمل الشيفرة.
مثال:
function User(name) { this.name = name; } User.prototype = {}; // (*) let user = new User('John'); let user2 = new user.constructor('Pete'); alert( user2.name ); // undefined
لمَ قيمة user2.name هي undefined؟
إليك طريقة عمل تعليمة new user.constructor('Pete'):
-
أولًا، تبحث عن المُنشِئ
constructorداخلuser، ولا تجده. -
ثمّ تتبع سلسلة prototype وتجد prototype الكائن
userهوUser.prototype، وأيضًا لا تجده. -
قيمة
User.prototypeما هي إلّا كائنًا فارغًا{}، و قيمة الخاصية prototype لهذا الكائن هيObject.prototype، وهنا وجدناObject.prototype.constructor == Objectبذلك استعملناه.
وفي نهاية الأمر، لدينا التعليمة let user2 = new Object('Pete') إذ أنّ الباني الخاص بالكائن Object يتجاهل الوسطاء وينشىء دائمًا كائنًا فارغًا. بطريقة مشابهة جدًا للتعليمة let user2 = {} والّتي أنشأت لنا الكائن user2 في نهاية الأمر.
ترجمة -وبتصرف- للفصل F.prototype من كتاب The JavaScript language

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