دليل تعلم جافاسكربت كائنات الدوال Function object وتعابير الدوال المسماة NFE في جافاسكربت


صفا الفليج

كما نعلم فالدوال في لغة جافاسكربت تُعدّ قيمًا.

ولكلّ قيمة في هذه اللغة نوع. ولكن ما نوع الدالة نفسها؟

تُعدّ الدوال كائنات في جافاسكربت.

يمكننا تخيّل الدوال على أنّها «كائنات إجرائية» يمكن استدعائها. لا يتوقّف الأمر عند الاستدعاء أيضًا بل يمكن حتّى أن نُعاملها معاملة الكائنات فنُضيف الخاصيات ونُزيلها، أو نمرّرها بالإشارة وغيرها من أمور.

خاصية الاسم name

تحتوي كائنات الدوال على خاصيات يمكننا استعمالها.

فمثلًا يمكن أن نعرف اسم الدالة من خاصية الاسم ‎name‎:

function sayHi() {
  alert("Hi");
}

alert(sayHi.name); // sayHi

والمُضحك في الأمر هو أنّ منطق اللغة في إسناد الاسم ذكيّ (الذكاء الاصطناعي مبهر)، فهو يُسند اسم الدالة الصحيح حتّى لو أنشأناها بدون اسم ثمّ أسندناها إلى متغير مباشرةً:

let sayHi = function() {
  alert("Hi");
};

alert(sayHi.name); // ‫sayHi (للدالة اسم!)

كما ويعمل المنطق أيضًا لو كانت عملية الإسناد عبر قيمة مبدئية:

function f(sayHi = function() {}) {
  alert(sayHi.name); // ‫sayHi (تعمل أيضًا!)
}

f();

تُدعى هذه الميزة في توصيف اللغة «بالاسم السياقي». فلو لم تقدّم الدالة اسمًا لها فيحاول المحرّك معرفته من السياق مع أوّل عملية إسناد.

كما ولتوابِع الكائنات أسماء أيضًا:

let user = {

  sayHi() {
    // ...
  },

  sayBye: function() {
    // ...
  }

}

alert(user.sayHi.name); // sayHi
alert(user.sayBye.name); // sayBye

ولكن ليس للسحر مكان هنا، فهناك حالات يستحيل على المحرّك معرفة الاسم الصحيح منها، بهذا تكون خاصية الاسم فارغة، كما في هذه الشيفرة:

// نُنشئ دالة في مصفوفة
let arr = [function() {}];

alert( arr[0].name ); // <سلسلة نصية فارغة>
// ما من طريقة يعرف بها المحرّك الاسم الصحيح، فباختصار، ليس هناك اسم!

ولكن عمليًا، لكل الدوال أسماء أغلب الوقت.

خاصية الطول length

توجد خاصية أخرى مضمّنة في اللغة باسم ‎length‎ وهي تُعيد عدد مُعاملات الدالة. مثال:

function f1(a) {}
function f2(a, b) {}
function many(a, b, ...more) {}

alert(f1.length); // 1
alert(f2.length); // 2
alert(many.length); // 2

نرى هنا بأن المُعاملات البقية لم تُحسب.

يستعمل المطوّرون خاصية ‎length‎ أحيانًا لإجراء التحقّق الداخلي داخل الدوال التي تعتمد في تشغيلها على التحكّم بدوال أخرى.

في الشيفرة أسفله، تقبل دالة ‎ask‎ سؤالًا ‎question‎ تطرحه وعددًا غير محدّد من دوال المعالجة ‎handler‎ لتستدعيها دالة السؤال.

فما إن ينزل الوحي على المستخدم ويُعطينا إجابة تستدعي الدالة المُعالجات. يمكننا تمرير نوعين اثنين من المُعالجات هذه:

  • دالة ليس لها وُسطاء لا تُنشأ إلا عندما يُعطيها المستخدم إجابة بالإيجاب.
  • دالة لها وُسطاء تُستدعى في بقية الحالات وتُعيد إجابة المستخدم.

علينا فحص خاصية ‎handler.length‎ لنستدعي ‎handler‎ بالطريقة السليمة.

الفكرة هنا هي أن تستعمل الشيفرة صياغة مُعالجة بسيطة وبدون وُسطاء لحالات الإيجاب (وهذا الشائع)، إضافةً على دعم المُعالجات العامة أيضًا:

function ask(question, ...handlers) {
  let isYes = confirm(question);

  for(let handler of handlers) {
    if (handler.length == 0) {
      if (isYes) handler();
    } else {
      handler(isYes);
    }
  }

}

// يُعاد كِلا المُعالجان لو كانت الإجابة بالإيجاب
// ولو كانت بالسلب، فالثاني فقط
ask("Question?", () => alert('You said yes'), result => alert(result));

هذه حالة من حالات التعدّدية الشكلية، أي حين يتغيّر تعاملنا مع الوُسطاء حسب أنواعها… في حالتنا فهي حسب أطوالها ‎length‎. لهذه الفكرة استعمال فعليّ في مكتبات جافاسكربت.

خاصيات مخصصة

يمكننا أيضًا إضافة ما نريد من خاصيات.

فهنا نُضيف خاصية العدّاد ‎counter‎ ليسجّل إجمالي عدد الاستدعاءات:

function sayHi() {
  alert("Hi");

  // لنعدّ كم من مرّة شغّلناه
  sayHi.counter++;
}
sayHi.counter = 0; // القيمة الأولية

sayHi(); // Hi
sayHi(); // Hi

alert( `‎Called ${sayHi.counter} times‎` ); // Called 2 times

الخاصيات ليست متغيرات لا تعرّف الخاصية المُسندة إلى الدوال مثل ‎sayHi.counter = 0‎ متغيرًا محليًا فيها (‎counter‎ في حالتنا). أي أنّ لا علاقة تربط الخاصية ‎counter‎ بالمتغير ‎let counter‎ البتة.

يمكننا التعامل مع الدوال على أنها كائنات فنخزّن فيها الخاصيات، ولكن هذا لا يؤثّر على طريقة تنفيذها. ليست المتغيرات خاصيات للدالة ولا العكس، كلاهما منفصلين يسيران في خطّين مُحال أن يتقاطعا.

يمكننا أحيانًا استعمال خاصيات الدوال بدل المُنغلِقات. فمثلًا يمكن إعادة كتابة تمرين دالة العدّ في فصل «المُنغلِقات» فنستعمل خاصية دالة:

function makeCounter() {
  // بدل:
  // let count = 0

  function counter() {
    return counter.count++;
  };

  counter.count = 0;

  return counter;
}

let counter = makeCounter();
alert( counter() ); // 0
alert( counter() ); // 1

هكذا خزّنا الخاصية ‎count‎ في الدالة مباشرةً وليس في بيئتها المُعجمية الخارجية.

أهذه أفضل أم المنغلقات أفضل؟

الفرق الرئيس هو: لو كانت قيمة ‎count‎ «تحيا» في متغير خارجي فلا يمكن لأي شيفرة خارجية الوصول إليها، بل الدوال المتداخلة فقط من يمكنها تعديلها، ولو ربطناها بدالة فيصير هذا ممكنًا:

function makeCounter() {

  function counter() {
    return counter.count++;
  };

  counter.count = 0;

  return counter;
}

let counter = makeCounter();

// هذا
counter.count = 10;
alert( counter() ); // 10

إذًا فالخيار يعود لنا: ماذا نريد وما الغاية.

تعابير الدوال المسماة

كما اسمها، فتعابير الدوال المسمّاة (Named Function Expression) هي تعابير الدوال التي لها اسم… بسيطة.

لنأخذ مثلًا تعبير دالة مثل أي تعبير تراه في حياتك البرمجية التعيسة:

let sayHi = function(who) {
  alert(`‎Hello, ${who}‎`);
};

والآن نُضيف اسمًا له:

let sayHi = function func(who) {
  alert(`‎Hello, ${who}‎`);
};

هل حللنا أزمة عالمية هنا؟ ما الداعي من هذا الاسم ‎"func"‎؟

أولًا، ما زال أمامنا تعبير دالة، فإضافة الاسم ‎"func"‎ بعد ‎function‎ لم يجعل الجملة تصريحًا عن دالة إذ ما زلنا نصنع الدالة داخل جزء من تعبير إسناد.

كما وإضافة الاسم لم يُعطب الدالة بأي شكل. يمكن أن ندخل الدالة هكذا ‎sayHi()‎:

let sayHi = function func(who) {
  alert(`‎Hello, ${who}‎`);
};

sayHi("John"); // Hello, John

ثمّة أمرين مميزين بهذا الاسم ‎func‎ وهما السبب وراء كل هذا:

  1. يتيح الاسم بأن تُشير الدالة إلى نفسها داخليًا.
  2. ولا يظهر الاسم لما خارج الدالة.

فمثلًا تستدعي الدالة ‎sayHi‎ أسفله نفسها ثانيةً بالوسيط ‎"Guest"‎ لو لم نمرّر لها ‎who‎ من البداية:

let sayHi = function func(who) {
  if (who) {
    alert(`‎Hello, ${who}‎`);
  } else {
    func("Guest"); // ‫نستعمل func لنستدعي نفسنا ثانيةً
  }
};

sayHi(); // Hello, Guest

// ولكن هذا لن يعمل:
func(); // ‫ويعطينا خطأً بأنّ func غير معرّفة (فالدالة لا تظهر لما خارجها)

ولكن لمَ نستعمل ‎func‎ أصلًا؟ ألا يمكن أن نستعمل ‎sayHi‎ لذلك الاستدعاء المتداخل؟

للصراحة، يمكن ذلك في حالات عديدة:

let sayHi = function(who) {
  if (who) {
    alert(`‎Hello, ${who}‎`);
  } else {
    sayHi("Guest");
  }
};

مشكلة تلك الشيفرة هي احتمالية تغيّر ‎sayHi‎ في الشيفرة الخارجية. فلو أُسندت الدالة إلى متغير آخر بدل ذاك فستبدأ الأخطاء تظهر:

let sayHi = function(who) {
  if (who) {
    alert(`‎Hello, ${who}‎`);
  } else {
    sayHi("Guest"); // ‫خطأ: sayHi ليست بدالة
  }
};

let welcome = sayHi;
sayHi = null;

welcome(); // ‫خطأ: لم يعد الاستدعاء المتداخل sayHi يعمل بعد الآن!

سبب ذلك هو أنّ الدالة تأخذ ‎sayHi‎ من بيئتها المُعجمية الخارجية إذ لا تجد ‎sayHi‎ محليًا فيها فتستعمل المتغير الخارجي. وفي لحظة الاستدعاء تلك يكون ‎sayHi‎ الخارجي قد صار ‎null‎.

وهذا الداعي من ذلك الاسم الذي نضعه في تعبير الدالة، أن يحلّ هذه المشاكل بذاتها.

هيًا نُصلح شيفرتنا:

let sayHi = function func(who) {
  if (who) {
    alert(`‎Hello, ${who}‎`);
  } else {
    func("Guest"); // الآن تعمل كما يجب
  }
};

let welcome = sayHi;
sayHi = null;

welcome(); // ‫Hello, Guest (الاستدعاءات المتداخلة تعمل)

الآن صارت تعمل إذ الاسم ‎"func"‎ محليّ للدالة فقط ولا تأخذها من الخارج (ولا تظهر للخارج أيضًا). تضمن لنا مواصفات اللغة بأنّها ستُشير دومًا وأبدًا إلى الدالة الحالية.

مع ذلك فما زالت الشيفرة الخارجية تملك المتغيرين ‎sayHi‎ و‎welcome‎، بينما ‎func‎ هو «اسم الدالة داخليًا» أي كيف تستدعي الدالة نفسها من داخلها.

ما من ميزة كهذه للتصريح عن الدوال ميزة «الاسم الداخلي» هذه التي شرحناها هنا مُتاحة لتعابير الدوال فقط وليست متاحة للتصريحات عن الدوال. فليس لهذه الأخيرة أية صياغة برمجية لإضافة اسم «داخلي» لها.

لكن أحيانًا نرى حاجة بوجود اسم داخلي نعتمد عليه، حينها يكون السبب وجيهًا بأن نُعيد كتابة التصريح عن الدالة إلى صيغة تعبير الدالة المسمّى.

ملخص

تُعدّ الدوال كائنات. شرحنا في الفصل خصائصها:

  • اسمها ‎name‎ -- غالبًا ما يأتي من تعريف الدالة. لكن لو لم يكن هناك واحد فيحاول المحرّك تخمينه من السياق (مثلًا من عبارة الإسناد).
  • عدد مُعاملتها في تعريف الدالة ‎length‎ -- لا تُحسب المُعاملات البقية.

لو عرّفنا الدالة باستعمال تعبير عن دالة (وليس في الشيفرة الأساس)، وكان لهذه الدالة اسم فنُسمّيها بتعبير الدالة المسمّى.

كما يمكن أن تحمل الدوال خاصيات إضافية، وتستغل الكثير من مكتبات جافاسكربت المعروفة هذه الميزة أيّما استغلال.

إذ تُنشئ دالة «رئيسة» بعدها تُرفق دوال أخرى «مُساعِدة» إليها. فمثلًا تُنشئ مكتبة jQuery الدالة بالاسم ‎$‎، وتُنشئ مكتبة lodash الدالة بالسم ‎_‎ ثمّ تُضيف خاصياتها ‎_.clone‎ و ‎_.keyBy‎ وغيرها (طالع docs متى أردت معرفتها أكثر). ما تفعله هذه الدوال يعود إلى أنّها (في الواقع) تحاول حدّ «التلوّث» في المجال العمومي فلا تستعمل المكتبة إلّا متغيرًا عموميًا واحدًا. وهذا يُقلّل من أدنى إمكانية لتضارب الأسماء.

إذًا، فالدالة تؤدي عملًا رائعًا كما هي، وأيضًا تحوي على وظائف أخرى خاصيات لها.

تمارين

ضبط قيمة العداد وإنقاصها

الأهمية: 5

عدّل شيفرة الدالة ‎makeCounter()‎ بحيث يُنقص العدّاد قيمتها إضافةً إلى ضبطها:

  • على ‎counter()‎ إعادة العدد التالي (كما في الأمثلة السابقة).
  • على ‎counter.set(value)‎ ضبط قيمة العدّاد لتكون ‎value‎.
  • على ‎counter.decrease()‎ إنقاص قيمة العدّاد واحدًا (1).

طالِع الشيفرة أدناه كي تعرف طريقة استعمال الدالة:

function makeCounter() {
  let count = 0;

  // ... شيفرتك هنا ...
}

let counter = makeCounter();

alert( counter() ); // 0
alert( counter() ); // 1

counter.set(10); // نضبط العدد الجديد

alert( counter() ); // 10

counter.decrease(); // نُنقص العدد واحدًا 1

alert( counter() ); // ‫10 (بدل 11)

ملاحظة: يمكنك استعمال مُنغلِق أو خاصية دالة لحفظ العدد الحالي، أو لو أردت فاكتب الحل بالطريقتين.

الحل

يستعمل الحل المتغير ‎count‎ محليًا، كما وتوابِع أخرى نكتبها داخل الدالة ‎counter‎. تتشارك هذه التوابِع ذات البيئة المُعجمية الخارجية كما وترى أيضًا قيمة ‎count‎ الحالية. 

مجموع ما في الأقواس أيًا كان عدد الأقواس

الأهمية: 5

اكتب الدالة ‎sum‎ لتعمل كالآتي:

sum(1)(2) == 3; // 1 + 2
sum(1)(2)(3) == 6; // 1 + 2 + 3
sum(5)(-1)(2) == 6
sum(6)(-1)(-2)(-3) == 0
sum(0)(1)(2)(3)(4)(5) == 15

تريد تلميحًا؟ ربما تكتب كائنًا مخصّصًا يُحوّل الأنواع الأولية لتُناسب الدالة.

  1. أيّما كانت الطريقة التي سنستعملها ليعمل هذا الشيء، فلا بدّ أن تُرجع ‎sum‎ دالة.
  2. على تلك الدالة أن تحفظ القيمة الحالية بين كلّ استدعاء والآخر داخل الذاكرة.
  3. حسب المهمّة المُعطاة، يجب أن تتحول الدالة إلى عدد حين نستعملها في ‎==‎. الدوال كائنات لذا فعملية التحويل ستنفع كما شرحنا في فصل «التحويل من كائن إلى قيمة أولية»، ويمكن أن نقدّم تابِعًا خاصًا يُعيد ذلك العدد.

إلى الشيفرة:

function sum(a) {

  let currentSum = a;

  function f(b) {
    currentSum += b;
    return f;
  }

  f.toString = function() {
    return currentSum;
  };

  return f;
}

alert( sum(1)(2) ); // 3
alert( sum(5)(-1)(2) ); // 6
alert( sum(6)(-1)(-2)(-3) ); // 0
alert( sum(0)(1)(2)(3)(4)(5) ); // 15

لاحظ بأنّ دالة ‎sum‎ تعمل مرّة واحدة فقط لا غير، وتُعيد الدالة ‎f‎.

وبعدها في كلّ استدعاء يليها، تُضيف ‎f‎ المُعامل إلى المجموع ‎currentSum‎ وتُعيد نفسها.

لا نستعمل التعاود في آخر سطر من ‎f‎.

هذا شكل التعاود:

function f(b) {
  currentSum += b;
  return f(); // <-- استدعاء تعاودي
}

بينما في حالتنا نُعيد الدالة دون استدعائها:

function f(b) {
  currentSum += b;
  return f; // <-- لا تستدعي نفسها، بل تُعيد نفسها
}

وستُستعمل ‎f‎ هذه في الاستدعاء التالي، وتُعيد نفسها ثانيةً مهما لزم. وبعدها حين نستعمل العدد أو السلسلة النصية، يُعيد التابِع ‎toString‎ المجموع ‎currentSum‎. يمكن أيضًا أن نستعمل ‎Symbol.toPrimitive‎ أو ‎valueOf‎ لإجراء عملية التحويل.

ترجمة -وبتصرف- للفصل Function object, NFE من كتاب The JavaScript language



1 شخص أعجب بهذا


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


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



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

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

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


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

تسجيل الدخول

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


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