كثيرٌ من الكائنات تستعمل الخاصية "prototype"
، حتّى في محرّك جافاسكربت، إذ تستعملها كلّ البواني المضمّنة في اللغة.
لنرى أولًا تفاصيلها وبعدها كيفية استعمالها لإضافة مزايا جديدة إلى الكائنات المضمّنة.
Object.prototype
لنقل بأنّا طبعنا كائنًا فارغًا:
let obj = {}; alert( obj ); // "[object Object]" ?
ما هذه الشيفرة الّتي ولّدت النصّ "[object Object]"
؟ هذه أفعال تابِع toString
المضمّن في اللغة، ولكن أين هذا التابِع فكائن obj
فارغ!
ولكن لو فكّرنا لحظة… فالاختصار هذا obj = {}
هو كأنما كتبنا obj = new Object()
، وهنا Object
هو الباني المضمّن في اللغة يُشير الخاصية prototype في الكائن إلى كائن آخر ضخم فيه التابِع toString
وغيره من توابِع.
هذا ما يحدث:
متى استدعينا new Object()
(أو أنشأنا كائن مجرّد {...}
)، ضُبطت الخاصية [[Prototype]]
لذاك الكائن إلى Object.prototype
طبقًا للقاعدة الّتي تحدّثنا عنها في الفصل السابق:
لذا متى حدث استدعاء إلى obj.toString()
أخذت لغة جافاسكربت التابِعَ من Object.prototype
.
يمكننا التأكّد من هذا هكذا:
let obj = {}; alert(obj.__proto__ === Object.prototype); // true // obj.toString === obj.__proto__.toString == Object.prototype.toString
لاحظ أنّ لم تعد هناك كائنات [[Prototype]]
في السلسلة فوق Object.prototype
:
alert(Object.prototype.__proto__); // null
كائنات النماذج الأولية الأخرى المضمّنة في اللغة
الكائنات الأخرى مثل المصفوفات Array
والتواريخ Date
والدوال Function
تضع هي الأخرى توابِعها في كائنات النماذج الأولية prototype.
فمثلًا، حين نُنشئ المصفوفة [1, 2, 3]
تستدعي لغة جافاسكربت داخليًا الباني new Array()
بنفسها، بذلك يصير كائن Array.prototype
كائنَ النموذج الأولي (prototype) ويقدّم له التوابِع اللازمة. هذا الأمر يزيد من كفاءة الذاكرة.
بحسب المواصفات القياسية للغة، أنّ لكلّ كائنات النماذج الأولية (prototype) المضمّنة كائنَ Object.prototype
آخر فوقها، ولهذا يقول الناس بأنّ ”كلّ شيء يرث الكائنات (Objects)“.
إليك صورة تصف هذا كله
لنرى أمر كائنات النماذج الأولية prototype يدويًا:
js runlet arr = [1, 2, 3]; // هل ترث Array.prototype؟ alert( arr.__proto__ === Array.prototype ); // true // ثمّ من Object.prototype؟ alert( arr.__proto__.__proto__ === Object.prototype ); // true // وفوق هذا كلّه null. alert( arr.__proto__.__proto__.__proto__ ); // null
أحيانًا تتداخل التوابِع في كائنات النماذج الأولية (prototype) مع بعضها. فمثلًا للكائن Array.prototype
تابِعًا خاصًا فيه toString
يعرض العناصر بينها فاصلة:
let arr = [1, 2, 3] alert(arr); // 1,2,3 <-- ناتج Array.prototype.toString
كما رأينا سابقًا فللكائن Object.prototype
تابِع toString
أيضًا، ولكنّ Array.prototype
أقرب في سلسلة وراثة النموذج الأولي prototype وبذلك تستعمل لغة جافاسكربت تابِع المصفوفة لا الكائن.
كما أنّ الأدوات في المتصفّحات (مثل طرفية كروم للمطوّرين) تعرض الوراثة (إن التعليمة console.dir
ربّما سنحتاجها للكائنات المضمنة في اللغة).
كما أنّ الكائنات الأخرى المضمّنة في اللغة تعمل بنفس الطريقة. حتى الدوالّ هم كائنات مبنية من خلال البواني المخصصة للدوالّ والمضمّنة في اللغة والدوالّ الخاصة بها (مثل الاستدعاء(call
)/التطبيق(apply
) وغيرهم من الدوالّ) مأخوذة من النموذج الأولي للدوالّ Function.prototype
. ولديهم دالّة toString
أيضًا.
function f() {} alert(f.__proto__ == Function.prototype); // true alert(f.__proto__.__proto__ == Object.prototype); // true، إذ ترث الكائنات
الأنواع الأولية
حتّى هنا لا تعقيد، إلّا حين نتعامل مع السلاسل النصية والأعداد والقيم المنطقية، عندها نرى التعقيدات تبدأ.
كما نتذكّر من فصول سابقة، فهذه الأنواع ليست كائنات، ولكن لو حاولنا الوصول إلى خاصياتها فسنرى كائنات تغليف أُنشئت مؤقتًا باستعمال البواني المضمّنة في اللغة String
و Number
و Boolean
، وهذه الكائنات تقدم ما نريد من توابِع وتختفي.
نرى هذه الكائنات مؤقّتًا إذ تُصنع سريعًا دون معرفتنا
إن القيم null
و undefined
لا تملك أي كائنٍ مغلّف لها، وليس لديها أي دوال ولا خاصيات ولا حتى نماذج أولية.
تغيير كائنات النماذج الأولية الأصيلة
يمكن تعديل كائنات النماذج الأولية الأصيلة. فمثلًا يمكننا إضافة تابِع إلى String.prototype
فيصبح متاحًا لكلّ السلاسل النصية:
String.prototype.show = function() { alert(this); }; "BOOM!".show(); // BOOM!
يمكن أن يراود المرء (وهو يطوّر) أفكار جديدة لتكون توابِع مضمّنة، ويريد إضافتها، بل نريد ونتوق إلى إضافتها إلى كائنات النماذج الأولية الأصيلة، إلّا أنّ هذه (وبصفة عامة) فكرة سيّئة جدًا.
تحذير بما أنّ النماذج الأولية نماذج عامة فمن السهل أن يحدث تضارب. إذ تأتي مكتبتين تُضيفان التابِع String.prototype.show
وتكتب واحدة على تابِع الأُخرى دون قصد.
لهذا تعدّ عملية تعديل كائنات النماذج الأولية الأصيلة على أنّها فكرة سيئة.
أمّا في البرمجة الحديثة، فما من طريقة مقبولة لتعديل كائنات النماذج الأولية الأصيلة إلا طريقة واحدة وهي: ترقيع نقص الدعم.
ترقيع نقص الدعم (أو Polyfilling) هو صناعة بديل عن تابِع توضّحه مواصفة جافاسكربت ولكنّه ليس مدعومًا في محرّك جافاسكربت الهدف.
لهذا السبب نكتب التنفيذ يدويًا ونضعه في كائن النموذج الأولي المضمّن له.
مثال:
if (!String.prototype.repeat) { // لو لم يكن هناك مثل هذا التابِع // نُضيفه إلى كائن prototype String.prototype.repeat = function(n) { // نكرّر السلسلة النصية n مرّة // في الواقع فالشيفرة الممتازة هي أكثر تعقيدًا من هذه // (تجد خوارزميتها الكاملة في المواصفة) // ولكن حتى التعويض الناقص يكون كافيًا أحيانًا كثيرة return new Array(n + 1).join(this); }; } alert( "La".repeat(3) ); // LaLaLa
الاستعارة من كائنات النماذج الأولية
تحدّثنا في الفصل "المُزخرِفات والتمرير، التابِعان call وapply"، عن استعارة التوابِع، أي حين نأخذ تابِعًا من كائن وننسخه إلى كائن غيره.
في أحيان كثيرة نستعير بعض توابِع كائنات النماذج الأولية الأصيلة. مثال على ذلك حين نصنع كائنًا يشبه المصفوفات ونريد نسخ بعض توابِع المصفوفات Array
إليه. انظر:
let obj = { 0: "Hello", 1: "world!", length: 2, }; obj.join = Array.prototype.join; // هنا alert( obj.join(',') ); // Hello,world!
تعمل الشيفرة أعلاه إذ أنّ الخوارزمية الداخلية لتابِع join
المضمّن لا يهمهّا إلا الفهارس الصحيحة وخاصية الطول length
، ولا ترى الكائن أهو حقًا مصفوفة أم لا. تتصرّف توابِع أخرى مضمّنة مثل تصرّف هذا التابِع.
يمكننا أيضًا الوراثة بضبط obj.__proto__
على Array.prototype
فتصير توابِع المصفوفات Array
مُتاحة للكائن obj
تلقائيًا.
ولكن ما إن يرث obj
من أيّ كائن آخر (غير كائن Array.prototype
) يصير هذا مستحيلًا. لا تنسَ بأنّا لا نستطيع الوراثة إلا من كائن واحد فقط لا غير.
تُعدّ هذه الميزة (ميزة استعارة التوابِع) ميزةً مرنة إذ تتيح لنا دمج مختلف مزايا الكائنات إن احتجناها.
خلاصة
-
تتبع كافة الكائنات المضمّنة في اللغة هذا النمط:
-
التوابِع مخزّنة داخل كائن النموذج الأولي (مثل
Array.prototype
وObject.prototype
وDate.prototype
وغيرها) - لا يخزّن الكائن إلّا بياناته (مثل عناصر المصفوفة وخصائص الكائن والتاريخ)
-
التوابِع مخزّنة داخل كائن النموذج الأولي (مثل
-
تخزّن الأنواع الأولية أيضًا توابِعها في كائنات النماذج الأولية لكائنات تغليف:
Number.prototype
وString.prototype
وBoolean.prototype
. فقطundefined
وnull
ليس لهما كائنات تغليف. - يمكنك تعديل كائنات النماذج الأولية المضمّنة في اللغة أو إضافة توابِع جديدة لها، ولكنّ تغييرها ليس أمرًا مستحسنًا. ربما تكون إضافة المعايير الجديدة (والّتي لا يدعمها محرّك جافاسكربت بعد) هي الحالة الوحيدة المسموح بها.
تمارين
إضافة التابع f.defer(ms) إلى الدوال
الأهمية: 5
أضِف إلى كائن النموذج الأولي المخصص للدوال التابِعَ defer(ms)
، ووظيفته تشغيل الدالة بعد ms
مِلّي ثانية.
بعدما تنتهي، يفترض أن تعمل هذه الشيفرة:
function f() { alert("Hello!"); } f.defer(1000); // تعرض ”Hello!“ بعد ثانية واحدة
الحل
Function.prototype.defer = function(ms) { setTimeout(this, ms); }; function f() { alert("Hello!"); } f.defer(1000); // تعرض ”Hello!“ بعد ثانية واحدة
إضافة المُزخرِف defer() إلى الدوال
الأهمية: 4
أضِف إلى كائن النموذج الأولي المخصص للدوالّ التابِعَ defer(ms)
، ووظيفته إعادة غلاف يُؤخّر الاستدعاء ms
مِلّي ثانية.
إليك مثالًا عن طريقة عمله:
function f(a, b) { alert( a + b ); } f.defer(1000)(1, 2); // يعرض ”3“ بعد ثانية واحدة
لاحِظ أنّ عليك تمرير الوُسطاء إلى الدالة الأصل.
الحل
Function.prototype.defer = function(ms) { let f = this; return function(...args) { setTimeout(() => f.apply(this, args), ms); } }; // نتأكّد function f(a, b) { alert( a + b ); } f.defer(1000)(1, 2); // يعرض ”3“ بعد ثانية واحدة
لاحظ بأنّا استعملنا this
في التابِع f.apply
لجعل المزخرف يعمل لكائن الدوالّ
لذا فإن استدعيَ تابع التغليف كدالّة كائن عندها ستمرر this
إلى الدالة الأصلية f
.
Function.prototype.defer = function(ms) { let f = this; return function(...args) { setTimeout(() => f.apply(this, args), ms); } }; let user = { name: "John", sayHi() { alert(this.name); } } user.sayHi = user.sayHi.defer(1000); user.sayHi();
ترجمة -وبتصرف- للفصل Native prototypes من كتاب The JavaScript language
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.