فى أول فصل من هذا الجزء تكلمنا عن أن هناك دوال جديدة لعمل نموذج (prototype).
تعتبر الخاصية __proto__ قديمة وغير مدعومة (فى عمل جافا سكريبت فى المتصفحات فقط).
الدوال الحديثة هي:
The modern methods are:
- Object.create(proto, [descriptors]) – creates an empty object with given
protoas[[Prototype]]and optional property descriptors. - Object.getPrototypeOf(obj) – returns the
[[Prototype]]ofobj. - Object.getPrototypeOf(obj) – تقوم بإرجاع الخاصية
[[Prototype]]من الكائنobj. - Object.setPrototypeOf(obj, proto) – تجعل الخاصية
[[Prototype]]من الكائنobjتشير إلىproto.
وهذه الدوال يجب استخدامها بلًا من __proto__.
عل ىسبيل المثال:
let animal = {
eats: true
};
// تقوم بإنشاء كان جديد حيث أن الكائن animal نموذج له
let rabbit = Object.create(animal);
alert(rabbit.eats); // true
alert(Object.getPrototypeOf(rabbit) === animal); // true
Object.setPrototypeOf(rabbit, {}); // تغيير نموذج الكائن rabbit إلى {}
تستقبل الدالة Object.create متغيرًا إضافيًا بشكل اختيارى وهو واصف الخاصية (property descriptors) حيث يمكننا إضافة خصائص إضافية للكائن الجديد كالآتى:
let animal = {
eats: true,
};
let rabbit = Object.create(animal, {
jumps: {
value: true,
},
});
alert(rabbit.jumps); // true
وتكون الواصفات على نفس الطريقة الموصوفة سابقًا فى فصل رايات الخصائص و واصفاتها.
يمكننا استخدام Object.create للقيام بنسخ كائن بشكل أفضل من نسخ الخصائص باستخدام التكرار for..in:
// كائن جديد مماثل تمامًا
let clone = Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj));
هذا الإستدعاء يقوم بإنشاء نسخه طبق الأصل من الكائن obj بما فيه من خصائص سواءًا كانت معدودة (enumerable) أم لا وكذلك الجالبات والمغيرات (getters & setters) – كل شيئ وبالخاصية [[Prototype]] الصحيحة.
نبذة من التاريخ
إذا عددنا كل الطرق للتحكم فى [[Prototype]]، فهناك الكثير! توجد الكثير من الطرق للقيام بنفس الشيئ!
لماذا؟
هذا لأسباب تاريخية متأصّلة.
- خاصية ال
"prototype"لدالة بانية (constructor function) موجودة من زمان بعيد. - لاحقًا فى عام 2012 ظهرت الدالة
Object.create. حيث تمكِّن من إنشاء كائنات بنموذج مُعطي ولكن لا تعطي الإمكانية لجلب أو تعديل الخصائص، ولذلك قامت المتصفحات بإضافة الخاصية__proto__الغير موثقة فى المصدر والتى تسمح للمستخدم أن يجلب أو يعدل النموذج فى أى وقت. - لاحقًا فى عام 2015 ظهرت الدالتين
Object.setPrototypeOfوObject.getPrototypeOfللقيام بنفس وظيفة الخاصية__proto__وحيث أن الخاصية__proto__موجودة فى كل مكان تقريبًا فقد أصبحت قديمة وأصبحت فى طريقها إلى (Annex B) من المصدر وبالتالى أصبحت اختيارية لبيئة جافا سكريبت غير المتصفحات.
والآن أصبح فى تصرفنا كل هذه الطرق.
لماذا تم استبدال الخاصية __proto__ بالدوال getPrototypeOf/setPrototypeOf؟ هذا سؤال مهم ويستدعينا أن نفهم لماذا تعد الخاصية __proto__ سيئة. أكمل القراءة لتحصل على الإجابة.
[[Prototype]]فى كائن موجود إذا كانت السرعة تهمكومحركات جافا سكريبت جاهزة للتعامل مع ذلك بسرعة فائقة. فتغيير النموذج وقت التنفيذ باستخدام Object.setPrototypeOf أو obj.__proto__= بطيئ جدًا ويبطئ عملية استجلاب الخصائص. ولذلك تجنب ذلك إلا إذا كنت تعرف ماذا تفعل أو أن السرعة لا تهمك.
الكائنات "العادية جدًا"
كما نعلم، فإنه يمكن استخدام الكائنات كقوائم مترابطة لتخزين خاصية بقيمتها.
…ولكن إذا حاولنا أن نخزن خصائص أعطاها المستخدم فيها فيمكننا أن نرى خللًا مهمًا: كل الخصائص تعمل جيدًا عدا "__proto__".
أنظر إلى هذا المثال:
let obj = {};
let key = prompt("What's the key?", "__proto__");
obj[key] = "some value";
alert(obj[key]); // [object Object], وليست "some value"!
هنا إذا قام المستخدم بكتابة __proto__، فإن ماكتبه سيتم تجاهله!
هذا لا يجب أن يفاجئنا، فالخاصية __proto__ لها تعامل خاص: لأنها يجب أن تكون كائنًا أو null، ولا يمكن أن يكون النص نموذجًا.
ولكننا لم نقصد أن نفعل ذلك، أليس كذلك؟ نريد أن نخزن خاصية بقيمتها واسم الخاصية "__proto__" لم يتم حفظه. فهذا إذن خلل!
الآثار هنا ليست كارثية، ولكن فى حالات أخرى يمكن أن نضع خاصية بقيمتها ثم يتغير النموذج بالفعل. ونتيجة لذلك سيعطى التنفيذ نتائج غير صحيحة وغير متوقعة.
وأسوأ من ذلك – لا يفكر المطورون عادة عن إمكانية كهذه أبدًا. وهذا يجعل الخطأ صعب الملاحظة ويمكن أن يتحول إلى ثغرة خصوصًا إذا كان البرنامج يعمل على السيرفر.
ويمكن أن تحدث أيضًا أشياء غير متوقعة عند وضع قيمة للدالة toString والتى هي دالة بطبيعتها وكذلك لدوال أخرى.
كيف يمكننا تجنب هذه المشكلة؟
أولًا، يمكننا أن نتحوّل لاستخدام الـMap للتخزين بدلًا من الكائنات العادية وسيكون كل شيئ بخير.
ولكن يمكن للـ Object أن يخدمنا بشكل جيد هنا، لأن صنّاع اللغة أعطو اهتمامًا لهذه المشكلة من وقت طويل.
إن الخاصية __proto__ ليست بخاصية عادية وإنما موصّل للخاصية Object.prototype:
ولذلك إذا كان استخدام obj.__proto__ للقراءة أو التعديل فإن الجالب أو المعدّل المناسب سيتم استدعاؤه من النموذج وستقوم بجلب أو تعديل الخاصية [[Prototype]].
كما قيل فى بداية هذا الجزء: __proto__ هي طريقة للوصول إلى الخاصية [[Prototype]] وليست الخاصية [[Prototype]] بنفسها.
والآن إذا أردنا أن نستخدم الكائن كقائمة مترابطة وخالية من مشاكل كهذه، يمكننا أن نفعل ذلك بحيلة بسيطة:
let obj = Object.create(null);
let key = prompt("What's the key?", "__proto__");
obj[key] = "some value";
alert(obj[key]); // "some value"
تنشئ Object.create(null) كائنًا ليس له نموذج ([[Prototype]] قيمتها null):
ولذلك ليس هناك جالب أو معدل موروث لـ __proto__. والآن يمكن التعامل معها كخاصية عادية وسيعمل المثال أعلاه بشكل صحيح.
يمكننا أن ندعو هذا الكائن “عادى جدًا” أو “very plain” أو “pure dictionary” لأنهم أبسط من الكائن العادى المعروف {...}.
ولكن العيب فى هذا أن هذا الكائن لا يحتوى بعض الدوال الموجودة بالفعل مثل toString:
let obj = Object.create(null);
alert(obj); // Error (no toString)
…ولكن عادةً ما يكون هذا جيدًا للقوائم المترابطة.
لا حظ أن أغلب الدوال المتعلقة بالكائن تتبع الصيغة Object.something(...), مثل Object.keys(obj) – فهم ليسو فى النموذج ولذلك مازال يمكن استخدامهم مع كائنات كهذه:
let chineseDictionary = Object.create(null);
chineseDictionary.hello = '你好';
chineseDictionary.bye = '再见';
alert(Object.keys(chineseDictionary)); // hello,bye
الملخص
الدوال الحديثة لإنشاء نموذج و الوصول إليه هي:
- Object.create(proto, [descriptors]) – creates an empty object with a given
protoas[[Prototype]](can benull) and optional property descriptors. - Object.getPrototypeOf(obj) – returns the
[[Prototype]]ofobj(same as__proto__getter). - Object.setPrototypeOf(obj, proto) – sets the
[[Prototype]]ofobjtoproto(same as__proto__setter).
و __proto__ الموجودة بالفعل والتي تقوم بجلب أو تعديل الخصائص ليست آمنة للإستخدام إذا كنا نريد أن ننشئ خصائص بأسماء يعطيها المستخدم للكائن وذلك لأن المستخدم يمكن أن يُدخل اسم الخاصية كـ "__proto__" وسيكون هناك خطأًا وبآثار ونتائج غير متوقعة.
لذا يمكننا استخدام Object.create(null) لإنشاء كائن عادى جدًا “very plain” بدون __proto__ أو استخدام الـ Map لهذا.
وأيضًا، تعطي الدالة Object.create طريقة سهل لنسخ الكائن بكل الواصفات (descriptors):
let clone = Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj));
وقد أوضحنا أيضًا أن __proto__ هو جالب أو معدّل للخاصية [[Prototype]] ويوجد فى Object.prototype مثل غيره من الدوال.
ويمكننا أن ننشئ كائنًا من غير نموذج باستخدام Object.create(null)، وهذه الكائنات تستخدم ككائنات عادية “pure dictionaries” حيث لا توجد لديها أى مشاكل إذا قام المستخدم بإدخال "__proto__" كإسم للخاصية.
دوال أخرى:
- Object.keys(obj) / Object.values(obj) / Object.entries(obj) – تقوم بإرجاع قائمة من خصائص الكائن المعدودة (enumerable) سواءًا أسماء أو قيم أو الإثنين معًأ.
- Object.getOwnPropertySymbols(obj) – تقوم بإرجاع قائمة بخصائص الكائن من نوع الرمز (symbolic properties).
- Object.getOwnPropertyNames(obj) – تقوم بإرجاع كل االخصائص من نوع النص (string properties).
- Reflect.ownKeys(obj) – تقوم بإرجاع قائمة بكل الخصائص التى يحتويها الكائن.
- obj.hasOwnProperty(key): returns
trueifobjhas its own (not inherited) key namedkey. - obj.hasOwnProperty(key): تقوم بإرجاع
trueإذا احتوي الكائن وليس نموذجه على خاصية تسمىkey.
كل الدوال التى تقوم بإرجاع خصائص الكائن (مثل Object.keys وغيرها) – تقوم بإرجاع الخصائص الموجودة فى الكائن فقط وليست الموجودة فى نموذجه (its prototype). فإذا كنا نريد إرجاع الموجودة فى النموذج أيضًا فيمكننا استخدام التكرار for..in.
التعليقات
<code>، وللكثير من السطور استخدم<pre>، ولأكثر من 10 سطور استخدم (plnkr, JSBin, codepen…)