تقدّم لنا لغة جافا سكريبت مرونة عالية غير مسبوقة في التعامل مع الدوال، إذ يمكننا تمريرها أو استعمالها على أنّها كائنات. والآن سنرى كيف نُمرّر الاستدعاءات بينها وكيف نُزخرِفها.
خبيئة من خلف الستار
لنقل بأنّ أمامنا الدالة الثقيلة على المعالج 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