Menu
العربية

Prototypes في JavaScript: سلسلة البروتوتايب بالتفصيل

تعرّف على الـ prototypes في جافا سكريبت وكيف تعمل سلسلة البروتوتايب في البحث عن الخصائص، وما علاقة صياغة class بهذه الآلية تحت الغطاء.

كل كائن في JavaScript له prototype

لغة JavaScript مبنية على مفهوم الـ prototype. الكلمة قد تبدو غامضة، لكن الفكرة في غاية البساطة: كل كائن يحمل رابطًا خفيًا إلى كائن آخر — يُسمى البروتوتايب الخاص به — وعندما تطلب خاصية غير موجودة في الكائن نفسه، يتبع JavaScript هذا الرابط ويبحث عنها هناك.

index.js
Output
Click Run to see the output here.

الكائن rabbit لا يملك خاصية eats خاصة به. تبدأ جافا سكريبت بالبحث داخل rabbit، وعندما لا تجدها تتبع رابط البروتوتايب وصولاً إلى animal، فتجد هناك eats: true وتُرجِعها. أما flies فتمشي على طول السلسلة ولا تعثر على شيء، فتُعيد undefined.

آلية البحث والتنقّل هذه هي جوهر الموضوع كله. الوراثة، والدوال، وحتى class — كلها مبنية على هذا المبدأ.

سلسلة البروتوتايب (Prototype Chain)

السلسلة لا تتوقف عند خطوة واحدة. فكل بروتوتايب يمكن أن يكون له بروتوتايب خاص به، وهكذا تستمر السلسلة إلى أن تصل إلى null:

index.js
Output
Click Run to see the output here.

شغّل الكود وسترى rabbit ثم Object.prototype ثم null. لهذا السبب تعمل rabbit.toString() رغم أنك لم تُعرّف toString في أي مكان — فهي موجودة أصلاً على Object.prototype، وهو قمة السلسلة لأي كائن تقريباً.

البحث عن الخصائص يتنقّل عبر سلسلة البروتوتايب من الأسفل إلى الأعلى. أما الإسناد فمختلف تماماً: يكتب دائماً على الكائن نفسه ولا يصعد للأعلى أبداً. هذا التباين مهم، ويوقع الكثيرين في الحيرة باستمرار.

الدوال البانية والخاصية .prototype

قبل ظهور class، كانت الطريقة المعتادة لإنشاء عدد من الكائنات المتشابهة هي استخدام دالة بانية (constructor function) مع الكلمة new:

index.js
Output
Click Run to see the output here.

يحدث شيئان عند استدعاء new User("Ada"):

  1. يُنشَأ كائن جديد، ويُضبط البروتوتايب الخاص به ليشير إلى User.prototype.
  2. تُستدعى User مع ربط this بهذا الكائن الجديد.

الدالة greet لا تُنسخ على كل نسخة (instance)، بل تعيش مرة واحدة فقط داخل User.prototype، ويصل إليها كلٌّ من ada وboris عبر تتبّع سلسلة البروتوتايب (prototype chain). ولهذا يطبع السطر الأخير true — فهي حرفيًا نفس الدالة.

الفرق بين prototype و __proto__

هذان الاسمان يُربكان الجميع. بينهما علاقة، لكنهما ليسا الشيء ذاته.

  • User.prototype هي خاصية موجودة على دالة الباني (constructor function). وهي الكائن الذي يصبح بروتوتايبًا للنسخ المُنشأة عبر new User(...).
  • ada.__proto__ (أو Object.getPrototypeOf(ada)) هو الرابط الموجود على النسخة نفسها، والذي يشير صعودًا إلى بروتوتايبها.
index.js
Output
Click Run to see the output here.

في الكود الجديد، يُفضَّل استخدام Object.getPrototypeOf(obj) بدلًا من obj.__proto__. فالـ __proto__ مجرد واجهة قديمة تُركت للحفاظ على التوافق، أما الدالة فهي الواجهة الرسمية المعتمدة.

الـ class في جافا سكريبت مجرد غلاف فوق الـ prototype

جافا سكريبت الحديثة تتيح لك كتابة class، لكن خلف الكواليس ما زلت تتعامل مع الـ prototypes نفسها. تأمّل النسختين جنبًا إلى جنب:

index.js
Output
Click Run to see the output here.

الدالة greet انتهى بها المطاف على User.prototype، تمامًا كما لو كنتَ كتبتها يدويًا. كلمة class في الغالب تمنحك صياغة أنظف، وقواعد أكثر صرامة (لا بد من استخدام new)، وطريقة أوضح للتعامل مع extends — لكن النموذج الفعلي وقت التشغيل لا يتغيّر.

فهم هذه النقطة مهم حين تقرأ رسائل الخطأ أو تُتتبّع قيمة this. فرسالة خطأ تذكر "User.prototype.greet" ليست اسمًا داخليًا غريبًا — إنها بالضبط المكان الذي تعيش فيه الدالة.

الوراثة في جافا سكريبت ما هي إلا سلسلة بروتوتايب أطول

الكلمة extends تربط بروتوتايبًا ببروتوتايب آخر، إذ يصبح بروتوتايب الأب هو بروتوتايب بروتوتايب الابن:

index.js
Output
Click Run to see the output here.

عند البحث عن rex.eat، يمشي جافا سكريبت في السلسلة من rex إلى Dog.prototype ثم إلى Animal.prototype، فيجد eat هناك ويستدعيها مع إبقاء this مرتبطًا بـ rex. هذا هو كل ما تفعله كلمة extends — فهي ببساطة تُجهّز لك سلسلة البروتوتايب.

إنشاء كائنات مباشرة عبر prototype

لستَ مضطرًا لاستخدام دالة باني (constructor) أصلًا. فالدالة Object.create(proto) تُنشئ كائنًا جديدًا يرث من البروتوتايب الذي تحدّده:

index.js
Output
Click Run to see the output here.

بدون class، وبدون new، وبدون دالة منشئة (constructor function). مجرد كائنين يتشاركان في method واحد عبر prototype مشترك. هذه هي الصورة الخام لـ prototypal inheritance في جافا سكريبت — وكل ما سواها مبني فوق هذه الفكرة.

hasOwnProperty: الخصائص الذاتية مقابل الموروثة

بما أن عملية البحث تسير عبر سلسلة البروتوتايب، فإن "foo" in obj ترجع true حتى للخصائص الموروثة. ولذلك عندما تريد التمييز بين الخصائص التي يملكها الكائن فعلاً وتلك الموروثة، استخدم Object.hasOwn (أو hasOwnProperty الأقدم):

index.js
Output
Click Run to see the output here.

الخاصية name موجودة على الـ instance نفسه، بينما greet موجودة على الـ prototype. العامل in يعثر على الاثنين معًا، أما Object.hasOwn فلا يلتقط إلا الخصائص المعرَّفة مباشرة على الكائن. هذا الفرق مهم جدًا عند التكرار باستخدام for...in أو عند تحويل الكائن إلى JSON، لأنك غالبًا لا تريد سوى الخصائص الذاتية (own properties).

لا تعبث بالـ prototypes المدمجة (Monkey-Patching)

بما أن Array.prototype مشترك بين كل المصفوفات في برنامجك، فأنت تستطيع نظريًا إضافة method إليه:

// من فضلك لا تفعل ذلك.
Array.prototype.last = function () {
    return this[this.length - 1];
};

[1, 2, 3].last(); // 3

المشكلة ليست أن الشيفرة لا تعمل — بل هي تعمل فعلاً. المشكلة أن كل مكتبة وكل اعتمادية وكل نسخة مستقبلية من JavaScript باتت تتشارك معك في نفس الـ namespace. فعندما تصدر Array.prototype.last يوماً ما كدالة رسمية بدلالات مختلفة قليلاً، ستنكسر شيفرتك (أو شيفرة غيرك) بطرق خفية يصعب تتبعها. وقصة Array.prototype.flatten / Array.prototype.flat هي المثال التحذيري الأشهر في هذا الباب.

اجعل الدوال المساعدة دوالاً مستقلة:

index.js
Output
Click Run to see the output here.

سطح مشترك أقل يعني احتكاكات أقل.

النموذج الذهني

لو جردنا الموضوع من كل التفاصيل، ستجد أن prototypes في JavaScript تتلخص في ثلاث قواعد:

  • كل كائن له رابط prototype (وقد يكون null).
  • قراءة الخصائص تصعد في السلسلة، أما الكتابة فلا.
  • class و new و extends ما هي إلا طرق لتجهيز هذه السلاسل دون الحاجة لكتابة Object.create بنفسك.

احتفظ بهذه القواعد الثلاث في ذهنك، وستجد أن سلوك this و instanceof وآلية استدعاء الـ methods والوراثة في جافا سكريبت كلها تتضح تلقائيًا.

التالي: حلقة الأحداث (Event Loop)

بهذا نكون قد أنهينا موضوع نموذج الكائنات. الفصل القادم ينقلنا إلى عالم مختلف تمامًا — كيف ينفذ JavaScript شفرتك فعليًا عبر الزمن. حلقة الأحداث هي ما يجعل المؤقتات و promises و async/await تتصرف بالشكل الذي نراه، وهي الأساس لكل ما يتعلق بالبرمجة غير المتزامنة.

الأسئلة الشائعة

ما هو الـ prototype في جافا سكريبت؟

كل كائن في جافا سكريبت يحتوي على رابط داخلي يشير إلى كائن آخر يُسمّى الـ prototype الخاص به. عندما تحاول الوصول إلى خاصية غير موجودة على الكائن نفسه، يصعد المحرك عبر هذا الرابط — أي سلسلة البروتوتايب (prototype chain) — بحثًا عنها. بهذه الطريقة يمكن تعريف دالة مرة واحدة ومشاركتها بين جميع النُسخ (instances).

ما الفرق بين __proto__ وprototype؟

prototype خاصية موجودة على دوال البناء (constructor functions) والـ classes، وهي الكائن الذي سيصبح prototype لكل نسخة تُنشَأ باستخدام new. أما __proto__ (أو Object.getPrototypeOf(obj)) فهو الرابط الفعلي على النسخة نفسها والذي يؤشر إلى الـ prototype الخاص بها. باختصار: instance.__proto__ === Constructor.prototype.

هل الـ classes في جافا سكريبت مجرد syntactic sugar فوق الـ prototypes؟

نعم، في معظمها. عند كتابة class Foo { bar() {} } فإن الدالة bar تُوضع على Foo.prototype تمامًا كما لو كتبت function Foo(){} ثم Foo.prototype.bar = function(){}. صحيح أن الـ classes أضافت الحقول الخاصة (private fields) ودلالات أكثر صرامة وصياغة أنظف لـ extends وsuper، لكن الآلية الأساسية تحت الغطاء لا تزال هي الـ prototypes.

هل من الجيد إضافة دوال إلى prototypes المدمجة مثل Array.prototype؟

تقريبًا أبدًا. أي تعديل على Array.prototype أو Object.prototype سينعكس على كل مصفوفة أو كائن في تطبيقك، بما في ذلك ما تأتي به المكتبات الخارجية. قد يتعارض ذلك مع إضافات مستقبلية للغة ويُفسد حلقات for...in. الأفضل أن تُبقي الدوال المساعدة داخل functions أو modules خاصة بك.

تعلّم البرمجة مع Coddy

ابدأ الآن