إن الخاصية "prototype"
مستخدمة بشكل واسع من جافا سكريبت نفسها، حيث أن كل الدوال البانية (constructor functions) تستخدمها.
أولًا سنرى التفاصيل، ثم نتعلم كيف نستخدمها لإضافة إمكانيات جديدة للكائنات الموجودة بالفعل (built-in objects).
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
alert(obj.toString === obj.__proto__.toString); //true
alert(obj.toString === Object.prototype.toString); //true
لاحظ أنه لا يوجد المزيد من [[Prototype]]
فى السلسة فوق Object.prototype
:
alert(Object.prototype.__proto__); // null
نماذج أخرى موجودة بالفعل (built-in prototypes)
إن الكائنات الأخرى الموجوده بالفعل مثل Array
, Date
, Function
وغيرهم يحتفظون بدوال فى النماذج (prototypes).
علي سبيل المثال، عندما نقوم بإنشاء قائمة [1, 2, 3]
، فإن الدالة البانية الموجودة بالفعل new Array()
يتم استخدامها داخليًا. ولذلك تصبح Array.prototype
النموذج الخاص بها وتمنحها دوال خاصة وهذا شيئ جيد جدًّا للذاكرة.
كما ذُكر فى المصدر، فإن كل النماذج (prototypes) الموجودة بالفعل لديها Object.prototype
على القمة فى الأعلى. وهذا مايدفع بعض الأشخاص للقول بأن “كل شيئ يرث من الكائنات”.
هنا الصورة الكاملة:
هيا نختبر الخاصية يدويًا:
let 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
بعض الدوال فى النماذج يمكن أن تتداخل، فعلى سبيل المثال، تملك Array.prototype
الدالة toString
الخاصة بها والتى تقوم بإرجاع نص يحوى عناصر القائمة وبينها الفاصلة:
let arr = [1, 2, 3];
alert(arr); // 1,2,3 <-- نتيجة Array.prototype.toString
كما رأينا سابقًا، تملك Object.prototype
أيضًا الدالة toString
ولكن Array.prototype
هي الأقرب فى السلسلة ولذلك يتم استخدام الدالة الخاصة بالقائمة.
تعرض الأدوات الموجودة فى المتصفح أيضًا الوراثة (يمكن أن يتم استخدام console.dir
مع بعض الكائنات الموجودة بالفعل ):
تعمل الكائنات الموجودة بالفعل الأخرى بنفس الطريقة. حتى الدوال – هي عبارة عن كائنات مبنية عن طريق الدالة البانية Function
والدوال الخاصه بها (مثل call
/apply
وغيرها) مأخوذة من Function.prototype
. وتحتوى الدوال على الدالة toString
الخاصة بها أيضًا.
function f() {}
alert(f.__proto__ == Function.prototype); // true
alert(f.__proto__.__proto__ == Object.prototype); // true, ترث من الكائنات
القيم المفردة Primitives
أكثر الأشياء المعقدة تحدث مع النصوص والأرقام والقيم المطلقة.
كما نتذكر فإنهم ليسو عبارة عن كائنات، ولكن إذا حاولنا أن نصل إلى خصائصهم فسيتم إحاطتها بكائن باستخدام الدوال البانية String
و Number
و Boolean
، حيث يمنحونهم الدوال ثم يختفون.
هذه الكائنات تم إنشاؤها لنا خفيةً وأغلب المحركات تقوم بتحسين ذلك، ولكن يصفها المصدر بهذه الطريقة بالضبط. ودوال هذه الكائنات توجد أيضًا فى النماذج وتكون متاحة كـ String.prototype
و Number.prototype
و Boolean.prototype
.
null
و undefined
ليس لها كائنات حاويةالقيم الخاصة null
و undefined
تقف بعيدًا عن هذا. حيث أنهم ليس لديهم كائنات حاوية (object wrappers)، ولذلك فإن الدوال والخصائص غير متاحة لهم وليس هناك نماذج لهم.
التعديل على النماذج البدائية (native prototypes)
يمكن تعديل النماذج البدائية. فعلى سبيل المثال، إذا أضفنا دالة إلى String.prototype
ستصبح متاحة إلى كل النصوص:
String.prototype.show = function() {
alert(this);
};
"BOOM!".show(); // BOOM!
وخلال التطبيق العملى يمكن أن تخطر لنا أفكار لدوال أخرى نريد أن ننشئها ويمكننا إضافتها للنماذج، ولكن هذا يُعد فكرة سيئة بشكل عام.
إن النماذج متاحة بشكل عام، ولذلك فإنه من السهل أن يحدث تعارض. فإذا كان هناك مكتبتان أضافتا نفس الدالة String.prototype.show
، إذن فإن واحدة منهن ستستبدل عمل الأخرى.
ولذلك، بشكل عام، فإن تعديل النماذج البدائية يُعد فكرة سيئة.
فى لغات البرمجة الحديثة، توجد حالة واحدة لتعديل النماذج البدائية. وهي تعدد الأشكال polyfilling
تعدد الأشكال هو مصطلح يعني إنشاء نسخه من دالة موجودة فى مصدر جافا سكريبت ولكنها غير مدعومة بعد من محرك جافا سكريبت معين.
يمكننا إذن كتابتها يدويّاً وإضافتها للنموذج.
على سبيل المثال:
if (!String.prototype.repeat) {
// إذا لم توجد هذه الدالة
// أضفها للنموذج
String.prototype.repeat = function (n) {
// كرر النص n من المرات
// فى الحقيقة، يجب أن يكون الكود أكثر تعقيدًا بقليل من هذا
// (the full algorithm is in the specification)
// ولكن حتي تعدد الأشكال الغير كامل غالبًا ما يكون كافيًا
return new Array(n + 1).join(this);
};
}
alert("La".repeat(3)); // LaLaLa
الإستعارة من النماذج
فى فصل المزخرفات decorators والتمرير forwarding: التابعان 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
يرث بالفعل من كائن آخر، تذكر أننا يمكننا أن نرث من كائن واحد فقط فى المرة الواحدة.
إن استعارة الدوال مرن ويسمح بمزج الوظائف من كائنات مختلفة إذا أردنا ذلك.
الملخص
- تتبع كل الكائنات الموجودة بالفعل نفس النمط:
- يتم الإحتفاظ بالدوال فى النموذج (prototype) (
Array.prototype
,Object.prototype
,Date.prototype
, إلخ.) - يتم تخزين البيانات فقط فى الكائن (عناصر قائمة أو خصائص كائن أو تاريخ)
- يتم الإحتفاظ بالدوال فى النموذج (prototype) (
- القيم المفردة (Primitives) تخزن الدوال فى نموذج خاص بالكائن الحاوي (wrapper object):
Number.prototype
وString.prototype
وBoolean.prototype
. ولا يوجد كائن حاوى للقيمتينundefined
وnull
. - النماذج الموجودة بالفعل (Built-in prototypes) يمكن تعديلها أو إضافة دوال جديدة لها، ولكن هذا غير موصيً به، وإن الحالة الوحيدة المسموح فيها بذلك هي عندما نريد أن نذيف وظيفة جديدة موجودة فى المصدر ولكنها مازالت غير مدعومة من محرك جافا سكريبت معين.