١٥ ديسمبر ٢٠٢١

أدوات النموذج والإستغناء عن الخاصية proto

فى أول فصل من هذا الجزء تكلمنا عن أن هناك دوال جديدة لعمل نموذج (prototype).

تعتبر الخاصية __proto__ قديمة وغير مدعومة (فى عمل جافا سكريبت فى المتصفحات فقط).

الدوال الحديثة هي:

The modern methods are:

وهذه الدوال يجب استخدامها بلًا من __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

الملخص

الدوال الحديثة لإنشاء نموذج و الوصول إليه هي:

و __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 وغيرها) – تقوم بإرجاع الخصائص الموجودة فى الكائن فقط وليست الموجودة فى نموذجه (its prototype). فإذا كنا نريد إرجاع الموجودة فى النموذج أيضًا فيمكننا استخدام التكرار for..in.

مهمه

يوجد كائن يسمي dictionary، تم إنشاؤه باستخدام Object.create(null) لتخزين خصائص بقيمها.

أضف الدالة dictionary.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__"

يمكن للدالة أن تأخذ كل الخصائص المعدودة (enumerable) باستخدام Object.keys وطباعة قائمة بهم.

لجعل الدالة toString غير معدودة (non-enumerable)، سنقوم بتعريفها باستخدام واصف (descriptor). ويسمح لنا شكل Object.create أن نضع واصفًا لخاصية كمتغير ثانٍ.

let dictionary = Object.create(null, {
  toString: {
    value() { // قيمتها عبارة عن دالة
      return Object.keys(this).join();
    }
  }
});

dictionary.apple = "Apple";
dictionary.__proto__ = "test";

// apple و __proto__ فى التكرار
for(let key in dictionary) {
  alert(key); // "apple", ثم "__proto__"
}

// comma-separated list of properties by toString
// قائمة من االخصائص مفصول بينها بالفاصلة
alert(dictionary); // "apple,__proto__"

عند إنشاء خاصية بواصف فإن مُعرِّفاتها تكون قيمها false. ولذلك فى الكود أعلاه فإن dictionary.toString هي غير معدودة (non-enumerable).

أنظر فصل رايات الخصائص و واصفاتها للمراجعة.

هيا نقوم بإنشاء كائن جديد يسمي 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
خريطة الدورة التعليمية