أسرار وظائف JavaScript

كل مبرمج على دراية بالوظائف. تحتوي الوظائف في JavaScript على العديد من الاحتمالات ، مما يسمح لنا بتسميتها "وظائف الترتيب الأعلى". ولكن حتى إذا كنت تستخدم ميزات JavaScript طوال الوقت ، فقد يكون لديهم شيء يفاجئك. في هذا المنشور ، سأغطي بعض الميزات المتقدمة لوظائف JavaScript. أتمنى أن تجد ما تتعلمه اليوم مفيدًا.











وظائف نقية



الوظيفة التي تفي بالمتطلبين التاليين تسمى نقية:



  • دائمًا ، عندما يتم استدعاؤها بنفس الوسيطات ، فإنها تعرض نفس النتيجة.
  • لا تحدث أي آثار جانبية عند تنفيذ الوظيفة.


لنفكر في مثال:



function circleArea(radius){
  return radius * radius * 3.14
}


إذا تم تمرير هذه الوظيفة بنفس القيمة radius، فإنها تُرجع دائمًا نفس النتيجة. في الوقت نفسه ، أثناء تنفيذ الوظيفة ، لا يتغير شيء خارجها ، أي أنه ليس لها أي آثار جانبية. كل هذا يعني أن هذه وظيفة خالصة.



إليك مثال آخر:



let counter = (function(){
  let initValue = 0
  return function(){
    initValue++;
    return initValue
  }
})()


لنجرب هذه الوظيفة في وحدة تحكم المتصفح.





اختبار الوظيفة في وحدة تحكم المستعرض



كما ترى ، تقوم الوظيفةcounterالتي تنفذ العداد بإرجاع نتائج مختلفة في كل مرة يتم استدعاؤها. لذلك ، لا يمكن تسميتها نقية.



وهنا مثال آخر:



let femaleCounter = 0;
let maleCounter = 0;
function isMale(user){
  if(user.sex = 'man'){
    maleCounter++;
    return true
  }
  return false
}


الموضح هنا هو دالة isMaleتقوم دائمًا بإرجاع نفس النتيجة عند تمرير نفس الوسيطة. لكن لها آثار جانبية. أي أننا نتحدث عن تغيير متغير عالمي maleCounter. نتيجة لذلك ، لا يمكن تسمية هذه الوظيفة نقية.



▍ لماذا هناك حاجة إلى وظائف نقية؟



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



1. رمز الوظائف الصرفة أوضح من كود الوظائف العادية ، فهو أسهل للقراءة



كل وظيفة نقية تهدف إلى مهمة محددة. يتم استدعاؤه بنفس المدخلات ، ويعيد نفس النتيجة دائمًا. هذا يحسن بشكل كبير من قابلية قراءة الكود ويجعل من السهل توثيقه.



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



افترض أن لديك جزءًا من التعليمات البرمجية مثل هذا:



for (int i = 0; i < 1000; i++){
    console.log(fun(10));
}


إذا كانت fun- هذه وظيفة ليست نقية ، فعند تنفيذ هذا الرمز يجب استدعاء هذه الوظيفة على أنها fun(10)1000 مرة.



وإذا كانت funوظيفة خالصة ، فيمكن للمترجم تحسين الكود. قد يبدو مثل هذا:



let result = fun(10)
for (int i = 0; i < 1000; i++){
    console.log(result);
}


3. وظائف نقية أسهل للاختبار



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



هذا مثال بسيط. تأخذ الدالة النقية مصفوفة من الأرقام كوسيطة وتضيف 1 إلى كل عنصر من تلك المصفوفة ، وتعيد مصفوفة جديدة. إليك تمثيل مختصر:



const incrementNumbers = function(numbers){
  // ...
}


لاختبار هذه الوظيفة ، يكفي كتابة اختبار وحدة يشبه ما يلي:



let list = [1, 2, 3, 4, 5];
assert.equals(incrementNumbers(list), [2, 3, 4, 5, 6])


إذا لم تكن الوظيفة نظيفة ، فعند اختبارها ، يجب أن تأخذ في الاعتبار العديد من العوامل الخارجية التي يمكن أن تؤثر على سلوكها. نتيجة لذلك ، سيكون اختبار مثل هذه الوظيفة أكثر صعوبة من اختبار وظيفة نقية.



وظائف ذات ترتيب أعلى.



وظيفة الترتيب الأعلى هي وظيفة لها واحدة على الأقل من القدرات التالية:



  • إنه قادر على قبول وظائف أخرى كحجج.
  • يمكنه إرجاع دالة كنتيجة لعمله.


يتيح لك استخدام وظائف ذات ترتيب أعلى زيادة مرونة التعليمات البرمجية الخاصة بك ، مما يساعدك على كتابة برامج أكثر إحكاما وفعالية.



لنفترض أن هناك مجموعة من الأعداد الصحيحة. من الضروري إنشاء مصفوفة جديدة بنفس الطول على أساسها ، ولكن على هذا النحو ، سيمثل كل عنصر منها نتيجة ضرب العنصر المقابل للمصفوفة الأصلية في اثنين.



إذا كنت لا تستخدم إمكانيات الوظائف ذات الترتيب الأعلى ، فقد يبدو حل هذه المشكلة كما يلي:



const arr1 = [1, 2, 3];
const arr2 = [];
for (let i = 0; i < arr1.length; i++) {
    arr2.push(arr1[i] * 2);
}


إذا كنت تفكر في المشكلة ، فقد تبين أن كائنات الكتابة Arrayفي JavaScript لها طريقة map(). هذه الطريقة تسمى map(callback). يقوم بإنشاء مصفوفة جديدة مليئة بعناصر المصفوفة التي تم استدعاؤها من أجلها ، ومعالجتها بالدالة التي تم تمريرها إليها callback.



هكذا يبدو حل هذه المشكلة باستخدام الطريقة كما map()يلي:



const arr1 = [1, 2, 3];
const arr2 = arr1.map(function(item) {
  return item * 2;
});
console.log(arr2);


الطريقة map()هي مثال على وظيفة ذات ترتيب أعلى.



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



نتائج وظيفة التخزين المؤقت



لنفترض أن لديك وظيفة نقية تبدو كالتالي:



function computed(str) {    
    // ,       
    console.log('2000s have passed')
      
    // ,     
    return 'a result'
}


من أجل تحسين أداء الكود ، لن يضرنا باللجوء إلى التخزين المؤقت لنتائج الحسابات التي يتم إجراؤها في الوظيفة. عند استدعاء مثل هذه الوظيفة بنفس المعلمات التي تم استدعاؤها بالفعل ، فلن تضطر إلى إجراء نفس الحسابات مرة أخرى. بدلاً من ذلك ، سيتم إرجاع نتائجها ، المخزنة مسبقًا في ذاكرة التخزين المؤقت ، على الفور.



كيف تجهز دالة بذاكرة تخزين مؤقت؟ للقيام بذلك ، يمكنك كتابة وظيفة خاصة يمكن استخدامها كغلاف للوظيفة الهدف. سنعطي هذه الوظيفة الخاصة اسما cached. تأخذ هذه الوظيفة دالة موضوعية كوسيطة وتعيد دالة جديدة. في إحدى الوظائف ، cachedيمكنك تنظيم التخزين المؤقت لنتائج استدعاء للوظيفة الملتفة حولها باستخدام كائن عادي ( Object) أو باستخدام كائن يمثل بنية بياناتMap...



هذا ما قد يبدو عليه كود الوظيفة cached:



function cached(fn){
  //     ,      fn.
  const cache = Object.create(null);

  //   fn,    .
  return function cachedFn (str) {

    //       -   fn
    if ( !cache[str] ) {
        let result = fn(str);

        // ,   fn,   
        cache[str] = result;
    }

    return cache[str]
  }
}


فيما يلي نتائج تجربة هذه الميزة في وحدة تحكم المتصفح.





تجربة وظيفة يتم تخزين نتائجها مؤقتًا



وظائف كسولة



في الهيئات الوظيفية ، توجد عادة بعض التعليمات لفحص بعض الشروط. في بعض الأحيان ، يجب التحقق من الشروط المقابلة لها مرة واحدة فقط. لا فائدة من التحقق منها في كل مرة يتم فيها استدعاء الوظيفة.



في مثل هذه الظروف ، يمكنك تحسين أداء الوظيفة عن طريق "حذف" هذه التعليمات بعد تنفيذها لأول مرة. نتيجة لذلك ، اتضح أن الوظيفة ، أثناء مكالماتها اللاحقة ، لن تضطر إلى إجراء فحوصات لم تعد ضرورية. ستكون هذه وظيفة "كسول".



لنفترض أننا بحاجة إلى كتابة دالة fooتُرجع دائمًا الكائن الذي Dateتم إنشاؤه في المرة الأولى التي يتم فيها استدعاء هذه الوظيفة. يرجى ملاحظة أننا بحاجة إلى كائن تم إنشاؤه بالضبط عند الاستدعاء الأول للوظيفة.



قد يبدو كودها كما يلي:



let fooFirstExecutedDate = null;
function foo() {
    if ( fooFirstExecutedDate != null) {
      return fooFirstExecutedDate;
    } else {
      fooFirstExecutedDate = new Date()
      return fooFirstExecutedDate;
    }
}


في كل مرة يتم استدعاء هذه الوظيفة ، يجب التحقق من الحالة. إذا كانت هذه الحالة صعبة للغاية ، فإن استدعاءات مثل هذه الوظيفة ستؤدي إلى انخفاض في أداء البرنامج. هذا هو المكان الذي يمكننا فيه استخدام تقنية إنشاء وظائف "كسولة" لتحسين الشفرة.



على وجه التحديد ، يمكننا إعادة كتابة الوظيفة على النحو التالي:



var foo = function() {
    var t = new Date();
    foo = function() {
        return t;
    };
    return foo();
}


بعد الاستدعاء الأول للوظيفة ، نستبدل الوظيفة الأصلية بالوظيفة الجديدة. ترجع هذه الوظيفة الجديدة القيمة tالتي يمثلها الكائن الذي Dateتم إنشاؤه في المرة الأولى التي تم فيها استدعاء الوظيفة. نتيجة لذلك ، لا يلزم فحص أي شروط عند استدعاء مثل هذه الوظيفة. يمكن لهذا الأسلوب تحسين أداء التعليمات البرمجية الخاصة بك.



كان هذا مثالًا شرطيًا بسيطًا جدًا. دعنا الآن ننظر إلى شيء أقرب إلى الواقع.



عند إرفاق معالجات الأحداث بعناصر DOM ، تحتاج إلى إجراء فحوصات للتأكد من أن الحل متوافق مع المتصفحات الحديثة ومع IE:



function addEvent (type, el, fn) {
    if (window.addEventListener) {
        el.addEventListener(type, fn, false);
    }
    else if(window.attachEvent){
        el.attachEvent('on' + type, fn);
    }
}


اتضح أنه في كل مرة نسمي دالة addEvent، يتم التحقق من شرط فيها ، وهو ما يكفي للتحقق مرة واحدة فقط ، عندما يتم استدعاؤها لأول مرة. لنجعل هذه الوظيفة "كسولة":



function addEvent (type, el, fn) {
  if (window.addEventListener) {
      addEvent = function (type, el, fn) {
          el.addEventListener(type, fn, false);
      }
  } else if(window.attachEvent){
      addEvent = function (type, el, fn) {
          el.attachEvent('on' + type, fn);
      }
  }
  addEvent(type, el, fn)
}


نتيجة لذلك ، يمكننا القول أنه إذا تم التحقق من شرط معين في دالة ، والتي يجب تنفيذها مرة واحدة فقط ، فعند تطبيق تقنية إنشاء وظائف "كسولة" ، يمكنك تحسين الكود. على وجه التحديد ، يتمثل التحسين في حقيقة أنه بعد الفحص الأول للشرط ، يتم استبدال الوظيفة الأصلية بوظيفة جديدة ، حيث لا توجد فحوصات أخرى للشروط.



وظائف كاري



الكاري هو مثل هذا التحول لوظيفة ، بعد تطبيق أي وظيفة كان يجب استدعاؤها قبل تمرير العديد من الحجج إليها في وقت واحد تتحول إلى وظيفة يمكن استدعاؤها عن طريق تمرير الحجج اللازمة لها واحدة تلو الأخرى.



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



ما هي الفائدة من هذا؟



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


ضع في اعتبارك وظيفة بسيطة تضيف الأرقام التي تم تمريرها إليها. دعنا نسميها add. يأخذ ثلاث معاملات كوسيطات ويعيد مجموعها:



function add(a,b,c){
 return a + b + c;
}


يمكن استدعاء هذه الوظيفة عن طريق تمرير حجج أقل مما تحتاجه (على الرغم من أن هذا سيؤدي إلى حقيقة أنها ترجع شيئًا مختلفًا تمامًا عما هو متوقع منها). يمكن أيضًا استدعاؤه باستخدام وسيطات أكثر مما تم توفيره عند إنشائه. في مثل هذه الحالة ، سيتم ببساطة تجاهل الحجج "غير الضرورية". قد تبدو تجربة وظيفة مماثلة كما يلي:



add(1,2,3) --> 6 
add(1,2) --> NaN
add(1,2,3,4) --> 6 //  .


كيف تحلى مثل هذه الوظيفة؟



فيما يلي رمز الوظيفة curryالتي تهدف إلى التعامل مع وظائف أخرى:



function curry(fn) {
    if (fn.length <= 1) return fn;
    const generator = (...args) => {
        if (fn.length === args.length) {

            return fn(...args)
        } else {
            return (...args2) => {

                return generator(...args, ...args2)
            }
        }
    }
    return generator
}


فيما يلي نتائج تجربة هذه الميزة في وحدة تحكم المتصفح.





تجربة الكاري في وحدة تحكم المتصفح



تكوين الوظيفة



لنفترض أنك بحاجة إلى كتابة دالة تقوم bitfishبإرجاع سلسلة ، عند أخذها كمدخلات ، على سبيل المثال ، سلسلة HELLO, BITFISH.



كما ترى ، تخدم هذه الوظيفة غرضين:



  • تسلسل الأوتار.
  • تحويل الأحرف في السلسلة الناتجة إلى أحرف كبيرة.


هذا ما قد يبدو عليه رمز هذه الوظيفة:



let toUpperCase = function(x) { return x.toUpperCase(); };
let hello = function(x) { return 'HELLO, ' + x; };
let greet = function(x){
    return hello(toUpperCase(x));
};


دعونا نجربها.





اختبار وظيفة في Browser Console



تتضمن هذه المهمة مهمتين فرعيتين تم تنظيمهما كوظائف منفصلة. نتيجة لذلك ، رمز الوظيفةgreetبسيط للغاية. إذا كان من الضروري إجراء المزيد من العمليات على السلاسل ، فستحتوي الوظيفةgreetعلى بناء مثلfn3(fn2(fn1(fn0(x)))).



دعنا نبسط حل المشكلة ونكتب دالة تتكون من دوال أخرى. دعنا نسميهاcompose. ها هو كودها:



let compose = function(f,g) {
    return function(x) {
        return f(g(x));
    };
};


الآن greetيمكن إنشاء الوظيفة باستخدام الوظيفة compose:



let greet = compose(hello, toUpperCase);
greet('kevin');


إن استخدام دالة composeلإنشاء دالة جديدة استنادًا إلى وظيفتين موجودتين يعني إنشاء دالة تستدعي هذه الوظائف من اليسار إلى اليمين. نتيجة لذلك ، نحصل على رمز مضغوط يسهل قراءته.



الآن composeتأخذ وظيفتنا معاملين فقط. ونود أن يكون قادرًا على قبول أي عدد من المعلمات.



تتوفر وظيفة مماثلة ، قادرة على قبول أي عدد من المعلمات ، في مكتبة التسطير السفلي المعروفة والمفتوحة المصدر .



function compose() {
    var args = arguments;
    var start = args.length - 1;
    return function() {
        var i = start;
        var result = args[start].apply(this, arguments);
        while (i--) result = args[i].call(this, result);
        return result;
    };
};


باستخدام تكوين الوظيفة ، يمكنك جعل العلاقات المنطقية بين الوظائف أكثر قابلية للفهم ، وتحسين قابلية قراءة الكود الخاص بك ، ووضع الأساس للامتدادات وإعادة البناء في المستقبل.



هل تستخدم أي طريقة خاصة للعمل مع الوظائف في مشاريع JavaScript الخاصة بك؟










All Articles