توابع Higher Order در جاوااسکریپت

توابع در جاوااسکریپت به عنوان اصلی‌ترین مفاهیم به‌شمار می‌آیند. می‌توانیم توابع را به ‌عنوان value در نظر بگیریم و به متغیر دیگر نسبت دهیم، آن‌ها را به‌عنوان آرگومان به تابع دیگری ارسال کنیم و یا حتی آن‌ها را از تابعی بازگردانیم. این توانایی توابع برای عمل به عنوان مفاهیم اصلی چیزی است که توابع higher order را در جاوااسکریپت تقویت می‌کند. اساساً تابعی که تابع دیگری را به عنوان آرگومان می‌گیرد یا تابعی را برمی‌گرداند به عنوان تابع higher order شناخته می‌شود.

در ادامه هر دو نوع پیاده‌سازی، یعنی ارسال یک تابع به عنوان آرگومان به تابع دیگر و بازگرداندن یک تابع از یک تابع دیگر را بررسی خواهیم کرد.

چگونه یک تابع را به عنوان آرگومان به تابع دیگری منتقل کنیم؟

در این بخش خواهیم دید که چگونه می‌توانیم یک تابع را به عنوان آرگومان ارسال کنیم و درنهایت این موضوع چگونه می‌تواند در نوشتن کدهای تمیزتر به ما کمک کند.

کد زیر را در نظر بگیرید، می‌خواهیم تابعی ایجاد کنیم که آرایه‌ای را به عنوان آرگومان بپذیرد. اعداد فرد را از آن فیلتر کند و تمام اعداد فیلتر شده را بازگرداند. به این صورت که:

const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];

function filterOdd(arr) {
  const filteredArr = [];
  for (let i = 0; i < arr.length; i++) {
    if (arr[i] % 2 !== 0) {
      filteredArr.push(arr[i]);
    }
  }
  return filteredArr;
}
console.log(filterOdd(arr));

// Output:
// [ ۱, ۳, ۵, ۷, ۹, ۱۱ ]

تابع بالا آرایه فیلتر شده [۱، ۳، ۵، ۷، ۹، ۱۱]را بازمی‌گرداند و همانطور که انتظار داشتیم فقط اعداد فرد را شامل می‌شود.

اکنون فرض کنید می‌خواهیم تابعی بسازیم که تمام اعداد زوج را فیلتر کرده و بازگرداند. برای این کار به صورت زیر عمل می‌کنیم:

function filterEven(arr) {
  const filteredArr = [];
  for (let i = 0; i < arr.length; i++) {
    if (arr[i] % 2 == 0) {
      filteredArr.push(arr[i]);
    }
  }
  return filteredArr;
}
console.log(filterEven(arr));

// Output:
// [ ۲, ۴, ۶, ۸, ۱۰ ]

باز هم همانطور که انتظار داشتیم خروجی دلخواه که یک آرایه با تمام اعداد زوج است، یعنی [۲، ۴، ۶، ۸، ۱۰]را دریافت می‌کنیم.

اما توجه داشته باشید که ما در این رویکرد کدهای تکراری زیادی داریم. هر دو توابع بالا کارهای مشترک زیادی مانند دریافت آرایه اصلی، ایجاد یک آرایه جدید برای ذخیره آرایه فیلتر شده، تشکیل loop روی آرایه اصلی و در نهایت بازگرداندن آرایه فیلتر شده را انجام می‌دهند.

تنها تفاوت بین هر دو تابع، منطقی است که برای فیلتر کردن آرایه اصلی استفاده می‌کنند.

برای تابع filterOddاز منطق arr[i] % 2 !== 0استفاده می‌کنیم در حالی که در تابع filterEvenبرای فیلتر کردن آرایه اصلی از منطق arr[i] % 2 == 0 استفاده کرده‌ایم.

اینجاست که می‌توانیم از توابع higher order بهره‌مند شویم. هدف اصلی این است که تابعی ایجاد کنیم تا تمام کارهای مشترکی که در دو تابع بالا وجود دارد را انجام دهد و سپس قسمت منطق توابع را جداگانه و به عنوان آرگومان به این تابع ارسال کنیم. برای انجام این کار مثلا می‌توانیم به شکل زیر عمل کنیم:

function filterFunction(arr, callback) {
  const filteredArr = [];
  for (let i = 0; i < arr.length; i++) {
    callback(arr[i]) ? filteredArr.push(arr[i]) : null;
  }
  return filteredArr;
}

فعلاً پارامتر callbackرا نادیده می‌گیریم. توجه داشته باشید که در تابع filterFuntionجدید همه مراحل مشترک در توابع filterOddو filterEvenرا حفظ کردیم.

اکنون پارامتر callback منطق را می‌پذیرد یعنی این پارامتر خود، تابع دیگری است که حاوی منطق filtering می‌باشد. برای فیلتر کردن اعداد فرد و زوج، توابع را به شکل زیر می‌نویسیم:

// Function containing logic for filtering out odd numbers

function isOdd(x) {
  return x % 2 != 0;
}

// Function containing logic for filtering out even numbers

function isEven(x) {
  return x % 2 === 0;
}

حالا باید آرایه اصلی را به همراه تابع اصلی که منطق برنامه را شامل می‌شود، به filterFunctionارسال کنیم:

// For filtering out odd numbers

filterFunction(arr, isOdd)
// Output of console.log(filterFunction(arr, isOdd)):
// [ ۱, ۳, ۵, ۷, ۹, ۱۱ ]

// For filtering out even numbers

filterFunction(arr, isEven)
// Output of console.log(filterFunction(arr, isEven)):
// [ ۲, ۴, ۶, ۸, ۱۰ ]

به این ترتیب ما توابع اصلی مانند isOddیا isEvenرا به عنوان آرگومان به تابع filterFunctionمنتقل می‌کنیم.

در این روش منطق فیلتر اصلی را از تابع اصلی جدا می‌کنیم. اکنون می‌توانیم هر منطق دیگری را همانطور که دوست داریم بدون نیاز به تغییر، به تابع filterFunction ارسال کنیم.

به عنوان مثال، اگر بخواهیم اعداد بزرگ‌تر از ۵ را فیلتر کنیم، فقط باید منطق برنامه را به شکل زیر بنویسیم:

function isGreaterThanFive(x) {
  return x > 5;
}

و آن را به عنوان آرگومان به filterFunction ارسال کنیم:

filterFunction(arr, isGreaterThanFive)

// Output of console.log(filterFunction(arr, isGreaterThanFive)):
// [ ۶, ۷, ۸, ۹, ۱۰, ۱۱ ]

همچنین می‌توانیم تابع اصلی را به ‌عنوان توابع arrow نیز ارسال کنیم و همان نتیجه را بگیریم، یعنی با نوشتن ((x) => x > 5)به جای isGreaterThanFive، همان نتیجه را دریافت می‌کنیم.

filterFunction(arr, (x) => x > 5)

// Output of console.log(filterFunction(arr, (x) => x > 5)):
// [ ۶, ۷, ۸, ۹, ۱۰, ۱۱ ]

نحوه ایجاد Polyfills

می‌دانیم که جاوااسکریپت برخی از توابع داخلی higher order مانند map()، filter()،reduce() و غیره را در اختیار ما قرار می‌دهد. اما آیا خودمان می‌توانیم این توابع را بازسازی کنیم؟

ما قبلاً تابع filtering را در بخش قبل ایجاد کردیم. اکنون یک array prototype از تابع filterFunctionخود ایجاد می‌کنیم تا بتوانیم آن را با هر آرایه‌ای استفاده کنیم. به عنوان مثال:

Array.prototype.filterFunction = function (callback) {
  const filteredArr = [];
  for (let i = 0; i < this.length; i++) {
    callback(this[i]) ? filteredArr.push(this[i]) : null;
  }
  return filteredArr;
};

در کد بالا، thisبه آرایه‌ای اشاره دارد که prototype فراخوانی شده است. بنابراین اگر کد را به صورت زیر بنویسیم:

const arr = [1, 2, 3, 4, 5]
arr.filterFunction(callbackFn)

در این صورت thisبه آرایه arrاشاره می‌کند.

اکنون می‌توانیم از filterFunction مانند تابع داخلی filter()در جاوااسکریپت استفاده کنیم. یعنی:

arr.filterFunction(isEven)

که شبیه فراخوانی تابع داخلی filter() است:

arr.filter(isEven)

هر دو فراخوانی تابع فوق یعنی arr.filterFunction(isEven)و arr.filter(isEven)خروجی یکسانی مانند [۲، ۴، ۶، ۸، ۱۰]را به ما می‌دهند.

همچنین می‌توانیم یک تابع arrow را همانطور که می‌توانیم به تابع داخلی filter()ارسال کنیم، به prototype نیز بفرستیم.

// I
arr.filterFunction((x) => x % 2 != 0)
arr.filter((x) => x % 2 != 0)
// both give the same output on console.log: [ 1, 3, 5, 7, 9, 11 ]

// II
arr.filterFunction((x) => x > 5)
arr.filter((x) => x > 5)
// both give the same output on console.log: [ 6, 7, 8, 9, 10, 11 ]

به نوعی ما یک polyfill برای تابع داخلی filter()نوشته‎‌ایم.

زنجیره توابع

می‌توانیم زنجیره‌سازی تابع را همانند تابع داخلی filter()، با اجرای prototype پیاده‌سازی کنیم. ابتدا تمام اعداد بزرگ‌تر از ۵ را فیلتر می‌کنیم. سپس از نتیجه‌ای که بدست آوردیم، تمام اعداد زوج را فیلتر می‌کنیم. به این صورت که:

// Using our own filterFunction() prototype implementation
arr.filterFunction((x) => x > 5).filterFunction((x) => x % 2 === 0)

//Using the inbuilt filter() implementation
arr.filter((x) => x > 5).filter((x) => x % 2 === 0)

// both give the same output on console.log: [ 6, 8, 10 ]

به این ترتیب می‌توانیم از توابع higher order در جاوااسکریپت برای نوشتن کدهای ماژولار تمیزتر و با قابلیت نگه‌داری بیشتر استفاده کنیم.

بازگرداندن یک تابع از یک تابع دیگر در جاوااسکریپت

ما می‌توانیم یک تابع را از یک تابع دیگر بازگردانیم زیرا توابع را در جاوااسکریپت به عنوان value در نظر می‌گیریم. این مفهوم را به کمک یک مثال بررسی می‌کنیم.

function calculate(operation) {
  switch (operation) {
    case "ADD":
      return function (a, b) {
        console.log(`${a} + ${b} = ${a + b}`);
      };
    case "SUBTRACT":
      return function (a, b) {
        console.log(`${a} - ${b} = ${a - b}`);
      };
  }
}

در کد بالا، وقتی تابع calculateرا با آرگومان فراخوانی می‌کنیم، آن آرگومان را تعویض می‌کند و در نهایت یک تابع anonymous را بازمی‌گرداند. بنابراین اگر تابع ()calculateرا فراخوانی کنیم و نتیجه آن را در یک متغیر ذخیره کنیم و در کنسول نمایش دهیم، خروجی زیر را دریافت خواهیم کرد:

const calculateAdd = calculate("ADD");
console.log(calculateAdd);

// Output: 
// [Function (anonymous)]

همانطور که می‌بینید متغیر calaculateAddحاوی یک تابع anonymous است که تابع calculate()آن را بازگردانده است.

دو راه برای فراخوانی این تابع داخلی وجود دارد که در ادامه به بررسی آن‌ها خواهیم پرداخت.

فراخوانی تابع برگشتی با استفاده از یک متغیر

در این روش همانند مثال بالا، تابع بازگشتی را در یک متغیر ذخیره می‌کنیم و سپس آن متغیر را فراخوانی می‌کنیم تا به نوبه خود تابع داخلی را فراخوانی کند.

برای درک بهتر آن مثال زیر را باهم بررسی می‌کنیم:

const calculateAdd = calculate("ADD");
calculateAdd(2, 3);
// Output: 2 + 3 = 5


const calculateSubtract = calculate("SUBTRACT");
calculateSubtract(2, 3);
// Output: 2 - 3 = -1

کارهایی که انجام داده‌ایم به ترتیب زیر است:

  • تابع calculate()را فراخوانی کردیم و ADDرا به عنوان آرگومان به آن ارسال کردیم
  • تابع anonymous بازگشتی را در متغیر calalogAddذخیره کردیم
  • درنهایت با فراخوانی calculateAdd()همراه با آرگومان‌های مورد نیاز، تابع بازگشتی داخلی را فراخوانی کردیم

فراخوانی تابع بازگشتی با استفاده از دو پرانتز

این یک روش بسیار پیچیده برای فراخوانی تابع بازگشتی داخلی است. در این روش از دو پرانتز ()()استفاده می‌کنیم.

برای اینکه آن را به خوبی درک کنیم مثال زیر را بررسی می‌کنیم:

calculate("ADD")(2, 3);
// Output: 2 + 3 = 5

calculate("SUBTRACT")(2, 3);
// Output: 2 - 3 = -1

در مورد این روش می‌توانیم به روشی مشابه با مثال زنجیره‌ای که در بالا داشتیم فکر کنیم. فقط در اینجا به جای زنجیره کردن توابع به یکدیگر، آرگومان‌ها را به هم زنجیره می‌کنیم.

آرگومان‌های پرانتز اول به تابع خارجی تعلق دارند، در حالی که آرگومان‌های پرانتز دوم متعلق به تابع بازگشتی داخلی هستند.

همانطور که قبلا توضیح دادیم متد calculate()یک تابع را برمی‌گرداند و آن همان تابع بازگشتی است که بلافاصله با استفاده از پرانتز دوم فراخوانی می‌شود.

یکی از جاهایی که می‌توانیم این نوع نمادگذاری دو پرانتز را ببینیم، متد connectدر کتابخانه مدیریت state مربوط به redux است. در این لینک می‌توانید بیشتر در مورد connect مطالعه کنید.

دیدگاه‌ها:

افزودن دیدگاه جدید