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

المزخرفات decorators‌ والتمرير forwarding: التابعان call وapply

تقدّم لنا لغة جافا سكريبت مرونة عالية غير مسبوقة في التعامل مع الدوال، إذ يمكننا تمريرها أو استعمالها على أنّها كائنات. والآن سنرى كيف نُمرّر الاستدعاءات بينها وكيف نُزخرِفها.

خبيئة من خلف الستار

لنقل بأنّ أمامنا الدالة الثقيلة على المعالج ‎slow(x)‎ بينما نتائجها مستقرة، أي لنقل بأنّنا لو مرّرنا ذات ‎x‎، فسنجد ذات النتيجة دومًا.

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

ولكن، بدل إضافة هذه الميزة في دالة ‎slow()‎ نفسها، سنُنشئ دالة غالِفة تُضيف ميزة الخبيئة هذه. سنرى أسفله مدى فوائد هذا الأمر.

إليك الشيفرة أولًا، وبعدها الشرح:

function slow(x) {
  // هنا مهمّة ثقيلة تُهلك المعالج
  alert(`‎Called with ${x}‎`);
  return x;
}

function cachingDecorator(func) {
  let cache = new Map();

  return function(x) {
    if (cache.has(x)) {    // لو وجدنا هذا المفتاح في الخبيئة
      return cache.get(x); // نقرأ النتيجة منها
    }

    let result = func(x);  // وإلّا نستدعي الدالة

    cache.set(x, result);  // ثمّ نُخبّئ (نتذكّر) ناتجها
    return result;
  };
}

slow = cachingDecorator(slow);

alert( slow(1) ); // slow(1) is cached and the result returned
alert( "Again: " + slow(1) ); // slow(1) result returned from cache

alert( slow(2) ); // slow(2) is cached and the result returned
alert( "Again: " + slow(2) ); // slow(2) result returned from cache

في الشيفرة أعلاه، ندعو ‎cachingDecorator‎ بالمُزخرِف (decorator): وهي دالة خاصّة تأخذ دالة أخرى مُعاملًا وتعدّل على سلوكها.

الفكرة هنا هي استدعاء ‎cachingDecorator‎ لأيّ دالة أردنا، وستُعيد لنا غِلاف الخبيئة ذاك. الفكرة هذه رائعة إذ يمكن أن نكون أمام مئات من الدوال التي يمكن أن تستغلّ هذه الميزة، وكلّ ما علينا فعله هو إضافة ‎cachingDecorator‎ عليها.

كما وأنّا نحافظ على الشيفرة أبسط بفصل ميزة الخبيئة عن مهمّة الدالة الفعلية.

ناتج ‎cachingDecorator(func)‎ هو «غِلاف» يُعيد الدالة ‎function(x)‎ التي «تُغلّف» استدعاء ‎func(x)‎ داخل شيفرة الخبيئة:

[decorator-makecaching-wrapper.png]

الشيفرات الخارجية لا ترى أيّ تغيير على دالة ‎slow‎ المُغلّفة. ما فعلناه هو تعزيز سلوكها بميزة الخبيئة.

إذًا نُلخّص: ثمّة فوائد عدّة لاستعمال ‎cachingDecorator‎ منفصلًا بدل تعديل شيفرة الدالة ‎slow‎ نفسها:

  • إعادة استعمال ‎cachingDecorator‎، فنُضيفه على دوال أخرى.
  • فصل شيفرة الخبيئة فلا تزيد من تعقيد دالة ‎slow‎ نفسها (هذا لو كانت معقّدة).
  • إمكانية إضافة أكثر من مُزخرف عند الحاجة (سنرى ذلك لاحقًا).

استعمال ‎func.call‎ لأخذ السياق

لا ينفع مُزخرِف الخبيئة الذي شرحناه مع توابِع الكائنات.

فمثلًا في الشيفرة أسفله، سيتوقّف عمل ‎worker.slow()‎ بعد هذه الزخرفة:

// ‫هيًا نُضف ميزة الخبيئة إلى worker.slow
let worker = {
  someMethod() {
    return 1;
  },

  slow(x) {
    // أمامنا مهمّة ثقيلة على المعالج هنا
    alert("Called with " + x);
    return x * this.someMethod(); // (*)
  }
};

// نفس الشيفرة أعلاه
function cachingDecorator(func) {
  let cache = new Map();
  return function(x) {
    if (cache.has(x)) {
      return cache.get(x);
    }
    let result = func(x); // (**)
    cache.set(x, result);
    return result;
  };
}

alert( worker.slow(1) ); // التابِع الأصلي يعمل كما ينبغي

worker.slow = cachingDecorator(worker.slow); // وقت الخبيئة

alert( worker.slow(2) ); // ‫لاااا! خطأ: تعذّرت قراءة الخاصية 'someMethod' في undefined

مكان الخطأ هو السطر ‎(*)‎ الذي يحاول الوصول إلى ‎this.someMethod‎ ويفشل فشلًا ذريعًا. هل تعرف السبب؟

السبب هو أنّ الغِلاف يستدعي الدالة الأصلية هكذا ‎func(x)‎ في السطر ‎(**)‎. وحين نستدعيها هكذا تستلم الدالة ‎this = undefined‎.

سنرى ما يشبه هذا الخطأ لو شغّلنا هذه الشيفرة:

let func = worker.slow;
func(2);

إذًا… يُمرّر الغِلاف الاستدعاء إلى التابِع الأصلي دون السياق ‎this‎، بهذا يحصل الخطأ.

وقت الإصلاح.

ثمّة تابِع دوال مضمّن في اللغة باسم func.call(context, …args) يتيح لنا استدعاء الدالة

صياغته هي:

func.call(context, arg1, arg2, ...)

يُشغّل التابِع الدالةَ ‎func‎ بعد تمرير المُعامل الأول (وهو ‎this‎) وثمّ مُعاملاتها.

للتبسيط، هذين الاستدعاءين لا يفرقان بشيء في التنفيذ:

func(1, 2, 3);
func.call(obj, 1, 2, 3)

فكلاهما يستدعي ‎func‎ بالمُعاملات ‎1‎ و‎2‎