٢٥ مارس ٢٠٢١

ربط الدوالّ Function binding

ثمّة مشكلة معروفة تواجهنا متى مرّرنا توابِع الكائنات على أنّها ردود نداء (كما نفعل مع ‎setTimeout‎)، هي ضياع هويّة الأنا ‎this‎.

سنرى في هذا الفصل طرائق إصلاح هذه المشكلة.

ضياع الأنا (الكلمة المفتاحية this)

رأينا قبل الآن أمثلة كيف ضاعت قيمة ‎this‎. فما نلبث أن مرّرنا التابِع إلى مكان آخر منفصلًا عن كائنه، ضاع ‎this‎.

إليك ظواهر هذه المشكلة باستعمال ‎setTimeout‎ مثلًا:

let user = {
  firstName: "John",
  sayHi() {
    alert(`‎Hello, ${this.firstName}!‎`);
  }
};

setTimeout(user.sayHi, 1000); // Hello, undefined!

كما رأينا في ناتج الشيفرة، لم نرحّب بالأخ «John» (كما أردنا باستعمال ‎this.firstName‎)، بل بالأخ غير المعرّف ‎undefined‎!

هذا لأنّ التابِع ‎setTimeout‎ استلم الدالة ‎user.sayHi‎ منفصلةً عن كائنها. يمكن أن نكتب السطر الأخير هكذا:

let f = user.sayHi;
setTimeout(f, 1000); // ‫ضاع سياق المستخدم user

بالمناسبة فالتابِع ‎setTimeout‎ داخل المتصفّحات يختلف قليلًا، إذ يضبط ‎this=window‎ حين نستدعي الدالة (بينما في Node.js يصير ‎this‎ هو ذاته كائن المؤقّت، ولكنّ هذا ليس بالأمر المهم الآن). يعني ذلك بأنّ ‎this.firstName‎ هنا هي فعليًا ‎window.firstName‎، وهذا المتغير غير موجود. عادةً ما تصير ‎this‎ غير معرّفة ‎undefined‎ في الحالات الأخرى.

كثيرًا ما نواجه هذه المسألة ونحن نكتب الشيفرة: نريد أن نمرّر تابِع الدالة إلى مكان آخر (مثل هنا، مرّرناه للمُجدول) حيث سيُستدعى من هناك. كيف لنا أن نتأكّد بأن يُستدعى في سياقه الصحيح؟

الحل رقم واحد: نستعمل دالة مغلفة

أسهل الحلول هو استعمال دالة غالِفة Wrapping function:

let user = {
  firstName: "John",
  sayHi() {
    alert(`‎Hello, ${this.firstName}!‎`);
  }
};

setTimeout(function() {
  user.sayHi(); // Hello, John!
}, 1000);

الآن اكتملت المهمة إذ استلمنا المستخدم ‎user‎ من البيئة المُعجمية الخارجية، وثمّ استدعينا التابِع كما العادة.

إليك ذات المهمة بأسطر أقل:

setTimeout(() => user.sayHi(), 1000); // Hello, John!

ممتازة جدًا، ولكن ستظهر لنا نقطة ضعف في بنية الشيفرة.

ماذا لو حدث وتغيّرت قيمة ‎user‎ قبل أن تعمل ‎setTimeout‎؟ (لا تنسَ التأخير، ثانية كاملة!) حينها سنجد أنّا استدعينا الكائن الخطأ دون أن ندري!

let user = {
  firstName: "John",
  sayHi() {
    alert(`‎Hello, ${this.firstName}!‎`);
  }
};

setTimeout(() => user.sayHi(), 1000);

// ‫...تغيّرت قيمة user خلال تلك الثانية
user = {
  sayHi() { alert("Another user in setTimeout!"); }
};

// setTimeout! هناك مستخدم آخر داخل التابِع‏

الحل الثاني سيضمن لنا ألّا تحدث هكذا أمور غير متوقّعة.

الحل رقم اثنين: ربطة

تقدّم لنا الدوال تابِعًا مضمّنًا في اللغة باسم bind يتيح لنا ضبط قيمة ‎this‎.

إليك صياغته الأساسية:

// ستأتي الصياغة المعقّدة لاحقًا لا تقلق
let boundFunc = func.bind(context);

ناتِج التابِع ‎func.bind(context)‎ هو «كائن دخيل» يشبه الدالة ويمكن لنا استدعائه على أنّه دالة، وسيمرّر هذا الاستدعاء إلى