فى أول فصل من هذا الجزء تكلمنا عن أن هناك دوال جديدة لعمل نموذج (prototype).
تعتبر الخاصية __proto__
قديمة وغير مدعومة (فى عمل جافا سكريبت فى المتصفحات فقط).
الدوال الحديثة هي:
The modern methods are:
- Object.create(proto, [descriptors]) – creates an empty object with given
proto
as[[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
proto
as[[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]]
ofobj
toproto
(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
true
ifobj
has its own (not inherited) key namedkey
. - obj.hasOwnProperty(key): تقوم بإرجاع
true
إذا احتوي الكائن وليس نموذجه على خاصية تسمىkey
.
كل الدوال التى تقوم بإرجاع خصائص الكائن (مثل Object.keys
وغيرها) – تقوم بإرجاع الخصائص الموجودة فى الكائن فقط وليست الموجودة فى نموذجه (its prototype). فإذا كنا نريد إرجاع الموجودة فى النموذج أيضًا فيمكننا استخدام التكرار for..in
.