دليل تعلم جافاسكربت توابع النماذج الأولية والكائنات بلا __proto__ في جافاسكربت


صفا الفليج

في أوّل فصل من هذا القسم قُلنا بأنّ هناك طرائق حديثة لكتابة الخاصية [[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:

object-prototype-2.png

لذا لو قرأنا 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):

object-prototype-null.png

يعني أنْ ليس هناك أيّ جالِب أو ضابِط موروثان للخاصية __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





تفاعل الأعضاء


لا توجد أيّة تعليقات بعد



يجب أن تكون عضوًا لدينا لتتمكّن من التعليق

انشاء حساب جديد

يستغرق التسجيل بضع ثوان فقط


سجّل حسابًا جديدًا

تسجيل الدخول

تملك حسابا مسجّلا بالفعل؟


سجّل دخولك الآن