في أوّل فصل من هذا القسم قُلنا بأنّ هناك طرائق حديثة لكتابة الخاصية [[prototype]].
يُعدّ التابِع __proto__
قديمًا وربّما نقول أيضًا لم يعد مستخدمًا (تحديدًا من جهة المتصفح في معايير جافاسكربت)
النسخ الحديثة هي:
-
Object.create(proto[, descriptors]) -- ينشئ كائنًا فارغًا بضبط
proto
الممرّر ليكون كائن[[Prototype]]
مع واصِفات الخاصيات الاختيارية لو مُرّرت. -
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
وسيط ثانٍ اختياري وهو: واصِفات الخاصيات، إذ يمكننا تقديم خاصيات إضافية إلى الكائن الجديد مباشرةً هكذا:
let animal = { eats: true }; let rabbit = Object.create(animal, { jumps: { value: true } }); alert(rabbit.jumps); // true
تنسيق الواصِفات هو نفس التنسيق الذي شرحناه في درس رايات الخاصيات وواصفاتها.
يمكننا أيضًا استعمال التابِع Object.create
لنسخ الكائنات بتحكّم أكبر من نسخ الخاصيات في حلقة for..in
:
// إنشاء نسخة مماثلة للكائن obj let clone = Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj));
يصنع هذا الاستدعاء نسخة حقيقية مطابقة عن obj
بما فيها الخاصيات: قابلية العدّ و منع قابلية العدّ وخاصيات البيانات و الضوابط/الجوالب - ينسخ كلّ شيء مع خاصية [[Prototype]]
.
لمحة تاريخية
لو عددنا الآن كلّ الطرائق التي نُدير فيها خاصية [[Prototype]]
لوجدناها لا تُحصى! الكثير من الطرق لمهمة واحدة. ولكن لماذا؟
طبعًا وكالعادة، لأسباب تاريخية.
-
بدأت الخاصية
"prototype"
للبواني (constructor) منذ زمن بعيد جدًا. -
ولاحقًا في 2012، ظهر التابِع
Object.create
في معيار اللغة. قدّم بذلك إمكانية صناعة الكائنات لها كائن نموذج أولي (prototype)، ولكن لم تقدّم أيّ طريقة لجلبه أو ضبطه. لهذا صنعت المتصفّحات تابع غير قياسي__proto__
ليصل إلى كائن النموذج الأولي ويُتيح للمستخدم جلبه وضبطه متى أراد. -
بعدها في 2015 أُضيف التابِعين
Object.setPrototypeOf
وObject.getPrototypeOf
إلى المعيار فيقوموا بنفس مقام التابع__proto__
. ولكن بحكم الأمر الواقع، فقد كان__proto__
موجودًا في كلّ مكان، وهكذا صار غير مستحسن استخدامه ونزل في المعيار إلى المُلحق باء (Annex B)، أي صار ”اختياريًا للبيئات التي ليست متصفّحات“.
والآن صرنا نملك هذه الطرائق الثلاث نتنعّم بها.
ولكن لماذا استبدلوا __proto__
بالدوال getPrototypeOf/setPrototypeOf
؟ سؤال مهم وعليه سنعرف ما السوء الكبير للتابِع __proto__
. واصِل القراءة لمعرفة ذلك.
ملاحظة:إن كانت السرعة عنصر مهم في مشروعك فلا تغيّر قيمة الخاصية [[Prototype]]
للكائنات الحالية. عادة ما نُسند هذه الخاصية [[Prototype]]
مرة واحدة فقط وذلك عند إنشاء الكائن ولا نعدلها بعد ذلك. ولكن بالطبع يمكننا استخدام الضابِط/الجالِب للخاصية [[Prototype]]
في أي وقت نريده. الكائن rabbit
يرث من الكائن animal
ولن يتغير هذا الأمر لاحقًا.
محرك لغة جافاسكربت محسّن على النحو الأمثل لهذا الغرض. إن عملية تغيير النموذج الأولي "على عجالة" باستخدام التابِع 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
. ممنوع أن تصير السلسلة النصية (String) كائنَ نموذج أولي. ولكن لم تكن النية أساسًا لتنفيذ هذا السلوك، صح؟ ما نريده هو تخزين أزواج المفاتيح والقيم، وهناك مفتاح اسمه "__proto__"
لم يُحفظ كما المفترض، والّذي أنشأ مشكلة!
هنا قد لا تكون العواقب وخيمة جدًا، ولكنها في حالات أخرى تكون، مثل لو كنّا نُسند قيم الكائنات، وبعدها تغيرت قيم النماذج الأولية لسبب ما لهذه الكائنات، عندها سيكون ناتج التنفيذ خاطئ وبطريقة غير متوقعة نهائيًا. الأنكى من هذا هو أنّ المطوّرين -عادةً- لا يفكّرون حتّى بإمكانية حدوث هذا، ما يصنع علل (Bugs) محال ملاحظتها، أو حتّى علل تصير ثغرات أمنية خصوصًا حين نستعمل جافاسكربت من جهة الخوادم.
كما تحدث أيضًا غرائب وعجائب ما إن أسندتَ شيئًا إلى التابِع toString
(وهو دالة بالوضع الإفتراضي) وغيره من توابِع أخرى مضمنّة في أصل اللغة. كيف لنا يا تُرى تجنّب هذا؟ أوّلا، نترك ما نستعمل وننتقل إلى الخارطة Map
، وهكذا نحلّ كلّ شيء. ولكن الكائنات نفسها Object
تكفي في هذه الحال إذ أنّ من صنع لغة جافاسكربت فكّر بهذه المشكلة قبل أن نُولد حتّى.
ليست __proto__
خاصية للكائن، بل خاصية وصول إلى Object.prototype
:
لذا لو قرأنا obj.__proto__
أو ضبطناها، فسيُستدعى الجالِب (أو الضابِط) من كائن النموذج الأولي (prototype) لها وتجلب/تضبط كائن [[Prototype]]
. فكما قلنا في بداية هذا القسم من الكتاب: ليست __proto__
إلّا طريقة للوصول إلى الخاصية [[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) (أي أنّ [[Prototype]]
يساوي null
):
يعني أنْ ليس هناك أيّ جالِب أو ضابِط موروثان للخاصية __proto__
، فهي الآن خاصية بيانات عادية وسيعمل مثالنا أعلاه كما نريد. نسمّي هذه الكائنات ”بالبسيطة جدًا“ (very plain) أو ”كائنات ليست إلّا قواميس“ إذ أنّها أبسط حتّى من الكائن البسيط (العادي) {...}
. العيب هنا هو أنّ ليس فيها أيّ توابِع كائنات مضمّنة مثل toString
:
let obj = Object.create(null); alert(obj); // Error (no toString)
ولكن ربّما ذلك يكون كافيًا للمصفوفات المترابطة
لاحظ كيف أنّ أغلب التوابِع المتعلّقة بالكائنات هي بالشكل Object.something(...)
(مثل Object.keys(obj)
) وليست موجودة في كائن النموذج الأولي (prototype)، وستعمل كما ينبغي لهذه الكائنات البسيطة:
let arabicDictionary = Object.create(null); chineseDictionary.hello = "hello"; chineseDictionary.bye = "bye"; alert(Object.keys(arabicDictionary)); // hello,bye
خلاصة
التوابِع الحديثة لضبط كائنات النماذج الأولية (prototype) والوصول إليها مباشرةً هي:
-
Object.create(proto[, descriptors]) -- ينشئ كائنًا فارغًا بضبط
proto
الممرّر ليكون كائن[[Prototype]]
(يمكن أن يحمل القيمةnull
) مع واصِفات الخاصيات الاختيارية لو مُرّرت. -
Object.getPrototypeOf(obj) -- يُعيد كائن
[[Prototype]]
للكائنobj
(مشابه لعمل الجالِب للخاصية__proto__
). -
Object.setPrototypeOf(obj, proto) -- يضبط الخاصية
[[Prototype]]
للكائنobj
لتكونproto
(مشابه لعمل الضابِط للخاصية__proto__
).
ليس من الآمن استعمال الجالِب/الضابِط __proto__
المضمّن في اللغة إن أردنا وضع مفاتيح صنعها المستخدم في الكائن. ليس ذلك آمنًا إذ يمكن أن يُدخل المستخدم "__proto__"
مفتاحًا وسنواجه خطأً له عواقب محالة التنبّؤ نأمل أن يُحمد عقباها.
لذا يمكننا إمّا استعمال التابِع Object.create(null)
لإنشاء كائن ”بسيط جدًا“ بدون استعمال __proto__
أو يمكننا استغلال كائنات الخرائط Map
وهو الأفضل. كما يمكننا أيضًا نسخ الكائنات مع جميع واصفاتِها من خلال التابع Object.create
.
let clone = Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj));
كما أنّا وضّحنا كيف أنّ __proto__
ليس إلّا جالِبًا/ضابِطًا للخاصية [[Prototype]]
وهو موجود تحت ظلّ Object.prototype
مثل غيره من التوابِع.
يمكننا إنشاء كائن ليس له نموذج أولي (prototype) باستعمال Object.create(null)
، وهكذا نستعملها على أنّها ”خرائط خام (Map)“، والجميل أنّها لا تُمانع قيمة مثل "__proto__"
لتكون مفتاحًا فيها.
توابِع أخرى:
- Object.keys(obj) / Object.values(obj) / Object.entries(obj) -- تعيد مصفوفة فيها جميع السلاسل (مفعلٌ بها خاصية قابلية العدّ) على شكل أسماء/قيم/أزواج مفاتيح-قيم.
- Object.getOwnPropertySymbols(obj) -- تُعيد مصفوفةً فيها جميع الخاصيات الرمزية (symbol properties) الموجودة مباشرةً في الكائن المعطي.
-
Object.getOwnPropertyNames(obj) -- تُعيد مصفوفةً فيها جميع الخاصيات التابعة مباشرةً للكائن المعطي، بما في ذلك الخاصيات غير القابلة للإحصاء (non-enumerable) لكن باستثناء خاصيات الرموز
Symbol
. - Reflect.ownKeys(obj) -- تعيد مصفوفة من جميع المفاتيح الخاصة بها.
-
obj.hasOwnProperty(key): تعيد
true
لو كان للكائنobj
خاصيةً ما مباشرةً (أي أنها لم يرثها).
تُعيد كلّ التوابِع التي تُعيد خاصيات الكائن (مثل Object.keys
وغيرها) خاصياتها ”هي“. لو أردنا تلك الموروثة فعلينا استعمال حلقة for..in
.
تمارين
إضافة toString
إلى dictionary
الأهمية: 5
لديك الكائن dictionary
حيث أنشأناه باستعمال Object.create(null)
ليخزّن أزواج key/value
أيًا كانت.
أضِف التابِع dictionary.toString()
فيه ليُعيد قائمة الخاصيات مفصولة بفواصل. يجب ألا يظهر التابِع toString
في حلقة for..in
عند مرورها على الكائن.
إليك طريقة عمل التابِع:
let dictionary = Object.create(null); // اكتب هنا الشيفرة التي تُضيف تابِع dictionary.toString // نُضيف بعض البيانات dictionary.apple = "Apple"; dictionary.__proto__ = "test"; // ليس __proto__ هنا إلّا خاصية لا أكثر // في الحلقة apple و __proto__ فقط for(let key in dictionary) { alert(key); // "apple"، ثمّ "__proto__" } // حان دور التابِع toString الذي كتبته alert(dictionary); // "apple,__proto__"
الحل
يمكن أن يأخذ التابِع كلّ الخاصيات (keys) القابلة للإحصاء باستعمال Object.keys
ويطبع قائمة بها.
لنمنع التابِع toString
من قابلية الإحصاء لنُعرّفه باستعمال واصِف خاصيات. تُتيح لنا صياغة التابِع Object.create
تقديم الكائن مع واصِفات الخاصيات كوسيط ثاني يمرر للتابِع.
// الشيفرة let dictionary = Object.create(null, { toString: { // نعرّف الخاصية toString value() { // قيمة الخاصية دالة return Object.keys(this).join(); } } }); dictionary.apple = "Apple"; dictionary.__proto__ = "test"; // في الحلقة apple و __proto__ for(let key in dictionary) { alert(key); // "apple"، ثمّ "__proto__" } // قائمة من الخاصيات طبعها toString مفصولة بفواصل alert(dictionary); // "apple,__proto__"
متى أنشأنا الخاصية باستعمال واصِف فستكون راياتها بقيمة false
مبدئيًا. إذًا في الشيفرة أعلاه التابِع dictionary.toString
ليس قابلًا للإحصاء.
ألقِ نظرة على درس "رايات الخاصيات وواصِفاتها" لتُنعش ذاكرتك.
الفرق بين الاستدعاءات
الأهمية: 5
لنُنشئ ثانيةً كائن rabbit
جديد:
function Rabbit(name) { this.name = name; } Rabbit.prototype.sayHi = function() { alert(this.name); }; let rabbit = new Rabbit("Rabbit");
هل تؤدّي هذه الاستدعاءات نفس المهمة أم لا؟
rabbit.sayHi(); Rabbit.prototype.sayHi(); Object.getPrototypeOf(rabbit).sayHi(); rabbit.__proto__.sayHi();
الحل
في الاستدعاء الأول يكون this == rabbit
، بينما في البقية يكون this
مساويًا إلى Rabbit.prototype
إذ أنّ الكائن الفعلي يكون قبل النقطة.
إذًا فالاستدعاء الأول هو الوحيد الذي يعرض Rabbit
، بينما البقية تعرض undefined
:
function Rabbit(name) { this.name = name; } Rabbit.prototype.sayHi = function() { alert( this.name ); } let rabbit = new Rabbit("Rabbit"); rabbit.sayHi(); // Rabbit Rabbit.prototype.sayHi(); // undefined Object.getPrototypeOf(rabbit).sayHi(); // undefined rabbit.__proto__.sayHi(); // undefined
ترجمة -وبتصرف- للفصل Prototype methods, objects without __proto__ من كتاب The JavaScript language
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.