دليل تعلم جافاسكربت النماذج الأولية الأصيلة (Native prototypes) في جافاسكربت


صفا الفليج

كثيرٌ من الكائنات تستعمل الخاصية "prototype"، حتّى في محرّك جافاسكربت، إذ تستعملها كلّ البواني المضمّنة في اللغة.

لنرى أولًا تفاصيلها وبعدها كيفية استعمالها لإضافة مزايا جديدة إلى الكائنات المضمّنة.

Object.prototype

لنقل بأنّا طبعنا كائنًا فارغًا:

let obj = {};
alert( obj ); // "[object Object]" ?

ما هذه الشيفرة الّتي ولّدت النصّ "[object Object]"؟ هذه أفعال تابِع toString المضمّن في اللغة، ولكن أين هذا التابِع فكائن obj فارغ!

ولكن لو فكّرنا لحظة… فالاختصار هذا obj = {}‎ هو كأنما كتبنا obj = new Object()‎، وهنا Object هو الباني المضمّن في اللغة يُشير الخاصية prototype في الكائن إلى كائن آخر ضخم فيه التابِع toString وغيره من توابِع.

هذا ما يحدث:

object-prototype.png

متى استدعينا new Object()‎ (أو أنشأنا كائن مجرّد {...})، ضُبطت الخاصية [[Prototype]] لذاك الكائن إلى Object.prototype طبقًا للقاعدة الّتي تحدّثنا عنها في الفصل السابق:

object-prototype-1.png

لذا متى حدث استدعاء إلى obj.toString()‎ أخذت لغة جافاسكربت التابِعَ من Object.prototype.

يمكننا التأكّد من هذا هكذا:

let obj = {};

alert(obj.__proto__ === Object.prototype); // true
// obj.toString === obj.__proto__.toString == Object.prototype.toString

لاحظ أنّ لم تعد هناك كائنات [[Prototype]] في السلسلة فوق Object.prototype:

alert(Object.prototype.__proto__); // null

كائنات النماذج الأولية الأخرى المضمّنة في اللغة

الكائنات الأخرى مثل المصفوفات Array والتواريخ Date والدوال Function تضع هي الأخرى توابِعها في كائنات النماذج الأولية prototype.

فمثلًا، حين نُنشئ المصفوفة [1, 2, 3] تستدعي لغة جافاسكربت داخليًا الباني new Array()‎ بنفسها، بذلك يصير كائن Array.prototype كائنَ النموذج الأولي (prototype) ويقدّم له التوابِع اللازمة. هذا الأمر يزيد من كفاءة الذاكرة.

بحسب المواصفات القياسية للغة، أنّ لكلّ كائنات النماذج الأولية (prototype) المضمّنة كائنَ Object.prototype آخر فوقها، ولهذا يقول الناس بأنّ ”كلّ شيء يرث الكائنات (Objects)“.

إليك صورة تصف هذا كله

native-prototypes-classes.png

لنرى أمر كائنات النماذج الأولية prototype يدويًا:

js runlet arr = [1, 2, 3];

// هل ترث ‫Array.prototype؟
alert( arr.__proto__ === Array.prototype ); // true 

// ثمّ من ‫Object.prototype؟
alert( arr.__proto__.__proto__ === Object.prototype ); // true 

// وفوق هذا كلّه ‫null.
alert( arr.__proto__.__proto__.__proto__ ); // null

أحيانًا تتداخل التوابِع في كائنات النماذج الأولية (prototype) مع بعضها. فمثلًا للكائن Array.prototype تابِعًا خاصًا فيه toString يعرض العناصر بينها فاصلة:

let arr = [1, 2, 3]
alert(arr); // 1,2,3 <-- ناتج Array.prototype.toString

كما رأينا سابقًا فللكائن Object.prototype تابِع toString أيضًا، ولكنّ Array.prototype أقرب في سلسلة وراثة النموذج الأولي prototype وبذلك تستعمل لغة جافاسكربت تابِع المصفوفة لا الكائن.

native-prototypes-array-tostring.png

كما أنّ الأدوات في المتصفّحات (مثل طرفية كروم للمطوّرين) تعرض الوراثة (إن التعليمة console.dir ربّما سنحتاجها للكائنات المضمنة في اللغة).

console_dir_array.png

كما أنّ الكائنات الأخرى المضمّنة في اللغة تعمل بنفس الطريقة. حتى الدوالّ هم كائنات مبنية من خلال البواني المخصصة للدوالّ والمضمّنة في اللغة والدوالّ الخاصة بها (مثل الاستدعاء(call)/التطبيق(apply) وغيرهم من الدوالّ) مأخوذة من النموذج الأولي للدوالّ Function.prototype. ولديهم دالّة toString أيضًا.

function f() {}

alert(f.__proto__ == Function.prototype); // true
alert(f.__proto__.__proto__ == Object.prototype); // true، إذ ترث الكائنات

الأنواع الأولية

حتّى هنا لا تعقيد، إلّا حين نتعامل مع السلاسل النصية والأعداد والقيم المنطقية، عندها نرى التعقيدات تبدأ.

كما نتذكّر من فصول سابقة، فهذه الأنواع ليست كائنات، ولكن لو حاولنا الوصول إلى خاصياتها فسنرى كائنات تغليف أُنشئت مؤقتًا باستعمال البواني المضمّنة في اللغة String و Number و Boolean، وهذه الكائنات تقدم ما نريد من توابِع وتختفي.

نرى هذه الكائنات مؤقّتًا إذ تُصنع سريعًا دون معرفتنا

إن القيم null و undefined لا تملك أي كائنٍ مغلّف لها، وليس لديها أي دوال ولا خاصيات ولا حتى نماذج أولية.

تغيير كائنات النماذج الأولية الأصيلة

يمكن تعديل كائنات النماذج الأولية الأصيلة. فمثلًا يمكننا إضافة تابِع إلى String.prototype فيصبح متاحًا لكلّ السلاسل النصية:

String.prototype.show = function() {
  alert(this);
};

"BOOM!".show(); // BOOM!

يمكن أن يراود المرء (وهو يطوّر) أفكار جديدة لتكون توابِع مضمّنة، ويريد إضافتها، بل نريد ونتوق إلى إضافتها إلى كائنات النماذج الأولية الأصيلة، إلّا أنّ هذه (وبصفة عامة) فكرة سيّئة جدًا.

تحذير بما أنّ النماذج الأولية نماذج عامة فمن السهل أن يحدث تضارب. إذ تأتي مكتبتين تُضيفان التابِع String.prototype.show وتكتب واحدة على تابِع الأُخرى دون قصد.

لهذا تعدّ عملية تعديل كائنات النماذج الأولية الأصيلة على أنّها فكرة سيئة.

أمّا في البرمجة الحديثة، فما من طريقة مقبولة لتعديل كائنات النماذج الأولية الأصيلة إلا طريقة واحدة وهي: ترقيع نقص الدعم.

ترقيع نقص الدعم (أو Polyfilling) هو صناعة بديل عن تابِع توضّحه مواصفة جافاسكربت ولكنّه ليس مدعومًا في محرّك جافاسكربت الهدف.

لهذا السبب نكتب التنفيذ يدويًا ونضعه في كائن النموذج الأولي المضمّن له.

مثال:

if (!String.prototype.repeat) { // لو لم يكن هناك مثل هذا التابِع
  // نُضيفه إلى كائن ‫prototype

  String.prototype.repeat = function(n) {
    // نكرّر السلسلة النصية ‫n مرّة

    // في الواقع فالشيفرة الممتازة هي أكثر تعقيدًا من هذه
    // (تجد خوارزميتها الكاملة في المواصفة)
    // ولكن حتى التعويض الناقص يكون كافيًا أحيانًا كثيرة
    return new Array(n + 1).join(this);
  };
}

alert( "La".repeat(3) ); // LaLaLa

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

تحدّثنا في الفصل "المُزخرِفات والتمرير، التابِعان call وapply"، عن استعارة التوابِع، أي حين نأخذ تابِعًا من كائن وننسخه إلى كائن غيره.

في أحيان كثيرة نستعير بعض توابِع كائنات النماذج الأولية الأصيلة. مثال على ذلك حين نصنع كائنًا يشبه المصفوفات ونريد نسخ بعض توابِع المصفوفات Array إليه. انظر:

let obj = {
  0: "Hello",
  1: "world!",
  length: 2,
};

obj.join = Array.prototype.join; // هنا

alert( obj.join(',') ); // Hello,world!

تعمل الشيفرة أعلاه إذ أنّ الخوارزمية الداخلية لتابِع join المضمّن لا يهمهّا إلا الفهارس الصحيحة وخاصية الطول length، ولا ترى الكائن أهو حقًا مصفوفة أم لا. تتصرّف توابِع أخرى مضمّنة مثل تصرّف هذا التابِع.

يمكننا أيضًا الوراثة بضبط obj.__proto__‎ على Array.prototype فتصير توابِع المصفوفات Array مُتاحة للكائن obj تلقائيًا.

ولكن ما إن يرث obj من أيّ كائن آخر (غير كائن Array.prototype) يصير هذا مستحيلًا. لا تنسَ بأنّا لا نستطيع الوراثة إلا من كائن واحد فقط لا غير.

تُعدّ هذه الميزة (ميزة استعارة التوابِع) ميزةً مرنة إذ تتيح لنا دمج مختلف مزايا الكائنات إن احتجناها.

خلاصة

  • تتبع كافة الكائنات المضمّنة في اللغة هذا النمط:
    • التوابِع مخزّنة داخل كائن النموذج الأولي (مثل Array.prototype و Object.prototype وDate.prototype وغيرها)
    • لا يخزّن الكائن إلّا بياناته (مثل عناصر المصفوفة وخصائص الكائن والتاريخ)
  • تخزّن الأنواع الأولية أيضًا توابِعها في كائنات النماذج الأولية لكائنات تغليف: Number.prototype و String.prototype و Boolean.prototype. فقط undefined و null ليس لهما كائنات تغليف.
  • يمكنك تعديل كائنات النماذج الأولية المضمّنة في اللغة أو إضافة توابِع جديدة لها، ولكنّ تغييرها ليس أمرًا مستحسنًا. ربما تكون إضافة المعايير الجديدة (والّتي لا يدعمها محرّك جافاسكربت بعد) هي الحالة الوحيدة المسموح بها.

تمارين

إضافة التابع f.defer(ms)‎ إلى الدوال

الأهمية: 5

أضِف إلى كائن النموذج الأولي المخصص للدوال التابِعَ defer(ms)‎، ووظيفته تشغيل الدالة بعد ms مِلّي ثانية.

بعدما تنتهي، يفترض أن تعمل هذه الشيفرة:

function f() {
  alert("Hello!");
}

f.defer(1000); // تعرض ‫”Hello!“ بعد ثانية واحدة

الحل

Function.prototype.defer = function(ms) {
  setTimeout(this, ms);
};

function f() {
  alert("Hello!");
}

f.defer(1000); // تعرض ‫”Hello!“ بعد ثانية واحدة

إضافة المُزخرِف defer()‎ إلى الدوال

الأهمية: 4

أضِف إلى كائن النموذج الأولي المخصص للدوالّ التابِعَ defer(ms)‎، ووظيفته إعادة غلاف يُؤخّر الاستدعاء ms مِلّي ثانية.

إليك مثالًا عن طريقة عمله:

function f(a, b) {
  alert( a + b );
}

f.defer(1000)(1, 2); // يعرض ”3“ بعد ثانية واحدة

لاحِظ أنّ عليك تمرير الوُسطاء إلى الدالة الأصل.

الحل

Function.prototype.defer = function(ms) {
  let f = this;
  return function(...args) {
    setTimeout(() => f.apply(this, args), ms);
  }
};

// نتأكّد
function f(a, b) {
  alert( a + b );
}

f.defer(1000)(1, 2); // يعرض ”3“ بعد ثانية واحدة

لاحظ بأنّا استعملنا this في التابِع f.apply لجعل المزخرف يعمل لكائن الدوالّ

لذا فإن استدعيَ تابع التغليف كدالّة كائن عندها ستمرر this إلى الدالة الأصلية f.

Function.prototype.defer = function(ms) {
  let f = this;
  return function(...args) {
    setTimeout(() => f.apply(this, args), ms);
  }
};

let user = {
  name: "John",
  sayHi() {
    alert(this.name);
  }
}

user.sayHi = user.sayHi.defer(1000);

user.sayHi();

ترجمة -وبتصرف- للفصل Native prototypes من كتاب The JavaScript language





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


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



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

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

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


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

تسجيل الدخول

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


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