١١ يوليو ٢٠٢٠

كائن الدالة و تعبير الدالة المُسَمَّى

كما نعرف جميعاً أن الدالة في الجافاسكريبت تعتبر قيمة.

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

في لغة الجافاسكريبت تعتبر الدوال ( أشياء/كائنات ) “Objects”.

و الطريقة الجيدة كي تستطيع تخيل الدوال هي أن تعتبرها أشياء تقوم ببعض الأفعال. لا نستطيع فقط مناداتها بل ومعاملتها كأنها أشياء أيضاً كإضافة و إزالة أي خاصية بداخلها أو تخزين مرجع يشير إليها و كل العمليات التي نقوم بها علي الأشياء.

خاصية “الإسم”

كائن الدالة يحتوي علي بعض الخواص التي يعاد استخدامها كثيراً

مثلاً إسم الدالة يعتبر متاح علي شكل خاصية الإسم “name property”.

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

alert(sayHi.name); // sayHi

والظريف فى الأمر ايضاً, منطق إضافة الإسم هنا يعتبر ذكياً. إنها ايضاً تضيف الإسم الصحيح للدالة حتي وإن كانت الدالة تم إنشائها بدون إسم ولكن تم إضافته بعد صنعها فوراً:

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

alert(sayHi.name); // sayHi (there's a name!)

إنها تعمل أيضاً في حال إذا كانت الإضافة تتم عن طريق قيمة إفتراضية:

function f(sayHi = function() {}) {
  alert(sayHi.name); // sayHi (works!)
}

f();

في مواصفات الدالة هذه الخاصية تسمي الإسم السياقي “contextual name”. وإذا كانت الدالة لا توفر لنا واحداً, فإن هذه الخاصية عند الإضافة تستطيع معرفته من السياق.

هذا الكلام يشمل طرق الأشياء أيضاً:

let user = {

  sayHi() {
    // ...
  },

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

}

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

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

// function created inside array
let arr = [function() {}];

alert( arr[0].name ); // <empty string>

// المحرك لم يجد طريقة لتسمية الإسم الصحيح, لذلك لا يجد إسم

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

The “length” property

هناك خاصية أخرى تمتلكها الدوال وهي خاصية الطول “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

هنا نلاحظ أن العوامل الباقية (rest parameters) لا تُعد.

خاصية الطول length تستخدم أحياناً لـإستبطان الذات داخل الدوال التي تعتمد على دوال اخرى

مثلاً الكود الذي فى الأسفل الدالة ask تقبل العامل question لتسأله وعدد عشوائي من الـ handler دوال لتنفيذها.

بمجرد أن يقدم المستخدم الإجابة, الدالة تنادي الـ handler. يمكن أن نمرر نوعين من الـ handlers:

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

لتتم مناداة الدالة handler بالطريقة الصحيحة, نفحص خاصية handlers.length.

الفكرة هى أن لدينا دالة بسيطة ليس لها عوامل للحالات الإيجابية, لكن تستطيع دعم الـ handlers ايضاً:

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 الأن تم تخزينه في الدالة مباشراً, ليس في حدسها الخارجي Lexical Environment

هل هذا يعتبر أفضل أم أسوء من إستخدام الإغلاق؟

الإختلاف الأساسي هو أن قيمة count تعيش داخل متغير خارجي لذلك الكود الخاردي لا يستطيع الوصول إليه. وحدها الدوال المتداخلة هي التي من الممكن أن تستطيع تعديله. لكن إذا كان مقيّداً بالدالة , إذاً هذا من السهل حدوثه وهذا هو الإختلاف بينهم:

function makeCounter() {

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

  counter.count = 0;

  return counter;
}

let counter = makeCounter();

counter.count = 10;
alert( counter() ); // 10

لذلك إختيار طريقة الكتابة والبناء تعتمد علي أهدافنا في المشروع.

تعبير الدالة المُسَمَّى

تعبير الدالة المُسَمَّى أو (NFE) اختصاراً لـ 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"); // لمناداة الدالة مرة أخرى
  }
};

sayHi(); // Hello, Guest

// لكن هذا لن يعمل لأن هذا الإسم لا يري في خارج تعريف الدالة :
func(); // Error, func is not defined (not visible outside of the function)

لماذا إستخدمنا 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"); // Error: sayHi is not a function
  }
};

let welcome = sayHi;
sayHi = null;

welcome(); // Error, the nested sayHi call doesn't work any more!

هذا حدث الدالة أخذت sayHi من حدثها الخارجي lexical environment. ولا يوجد متغير محلي اسمه sayHi, لذلك المتغير الخارجى مستخدم. و وفى لحظة المناداة sayHi يعتبر null.

هنا يأتي دور الإسم الذى وضعناه داخل تعبير الدالة.

هو من يحل هذه المشاكل.

دعنا نستخدم هذا ونصلح الكود الخاص بنا.

let sayHi = function func(who) {
  if (who) {
    alert(`Hello, ${who}`);
  } else {
    func("Guest"); // Now all fine
  }
};

let welcome = sayHi;
sayHi = null;

welcome(); // Hello, Guest (nested call works)

الأن هذا يعمل, لأن الإسم "func" يعتبر دالة محلية. هذا لا يؤخذ من الخارج. هذه الخاصية تضمن لنا أنها ستكون دائماً تشير إلي الدالة الحالية.

الكود الخارجي يظل يملك المتغير sayHi أو welcome. و func تعتبر إسم داخلي للدالة.

لا يوجد هذا الشئ في تعريف الدالة العادية

خاصية إسم دالة متاحة فقط لتعبير الدالة Function Expressions وليس لتعريف الدالة Function Declarations. بالنسبة لتعريف الدالة لا توجد طريقة لتعريف إسم داخلي.

في بعض الأحيان عندما نحتاج إلي إسم داخلي يكون هذا السبب لتحويل تعريف الدالة إلى NFE أو تعبير الدالة المُسمى.

المُلخص

الدوال أشياء.

هنا سنغطي ما شرحناه مسبقاً عن خصائصها :

  • الإسم name – إسم الدالة. عادة يؤخذ من تعريف الدالة, ولكن إذا كان لا يوجد إسم. الجافاسكريبت تحاول تخمينه من السياق.
  • الطول length – عدد العوامل في تعريف الدالة. والعوامل الباقية Rest parameters لا تّحسب.

إذا تم تعريف الدالة علي أنها تعبير دالة وتحمل إسم, ذلك يدعي NFE تعبير الدالة المُسَمَّى. الإسم يمكن أن يستخدم في داخل الدالة لتشير إلي نفسها, للتكرار أو أي شئ

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

هم يصنعوا دالة أساسية ويلحق بها دوال أخرى مساعدة لها. مثلاً مكتبة الـ jQuery تصنع دالة تُسمي $. مكتبة lodash تصنع دالة _, ثم تضيف _.clone و _.keyBy و خواص أخرى لها. يمكنك أن تعرف أكثر عن هذه المكتبة من هنا docs. هم يفعلون ذلك في الواقع لتقليل تلوث الفراغ الشامل, لذلك المكتبة الواحدة تعطي فقط متغير عالمي. ذلك يقلل إحتمالية تداخل الأسماء.

لذلك الدالة يمكنها القيام بواجب رائع من نفسها ويمكن أن تحمل الكثير من الخواص الأخري في داخلها.

مهمه

الأهمية: 5

عدل الكود الخاص بالدالة makeCounter() بحيث يمكن للعداد أيضاً أن يخفض أو يضبط العدد:

  • counter() يجب أن تُرجِع الرقم التالي (كما في السابق).
  • counter.set(value) يجب أن تضع قيمة العداد إلي value.
  • counter.decrease() يجب أن تقلل قيمة العداد بفارق واحد.

إنظر لصندوق الكود بالأسفل لإستخدام المثال كاملاً

ملحوظة: يمكنك إستخدام إما الإغلاق أو خاصية الدالة للمحافظة على العداد الحالي. أو تستخدم الإثنين إذا أردت.

افتح sandbox بالإختبارات.

الحل يستخدم count في المتغير المحلي، لكن الوظائف الإضافية تمت كتابتها بجانب counter. إنهم يتشاركون نفس البيئة المعجمية الخارجية ويمكنهم أيضاً الوصول إلى القيمة الحالية count.

function makeCounter() {
  let count = 0;

  function counter() {
    return count++;
  }

  counter.set = value => count = value;

  counter.decrease = () => count--;

  return counter;
}

افتح الحل الإختبارات في sandbox.

الأهمية: 2

إكتب دالة 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

تلميح: ربما تحتاج إلي كائن مخصوص للتحويل البدائي لدالتك.

افتح sandbox بالإختبارات.

  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

function sum(a) {

  let currentSum = a;

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

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

  return f;
}

افتح الحل الإختبارات في sandbox.

خريطة الدورة التعليمية

التعليقات

إقرأ هذا قبل أن تضع تعليقًا…
  • إذا كان لديك اقتراحات أو تريد تحسينًا - من فضلك من فضلك إفتح موضوعًا فى جيتهاب أو شارك بنفسك بدلًا من التعليقات.
  • إذا لم تستطع أن تفهم شيئّا فى المقال - وضّح ماهو.
  • إذا كنت تريد عرض كود استخدم عنصر <code> ، وللكثير من السطور استخدم <pre>، ولأكثر من 10 سطور استخدم (plnkr, JSBin, codepen…)