توابع در جاوااسکریپت به عنوان اصلیترین مفاهیم بهشمار میآیند. میتوانیم توابع را به عنوان 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)): // [ ۶, ۷, ۸, ۹, ۱۰, ۱۱ ]
میدانیم که جاوااسکریپت برخی از توابع داخلی 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
را به عنوان آرگومان به آن ارسال کردیمcalalogAdd
ذخیره کردیمcalculateAdd()
همراه با آرگومانهای مورد نیاز، تابع بازگشتی داخلی را فراخوانی کردیماین یک روش بسیار پیچیده برای فراخوانی تابع بازگشتی داخلی است. در این روش از دو پرانتز ()()
استفاده میکنیم.
برای اینکه آن را به خوبی درک کنیم مثال زیر را بررسی میکنیم:
calculate("ADD")(2, 3); // Output: 2 + 3 = 5 calculate("SUBTRACT")(2, 3); // Output: 2 - 3 = -1
در مورد این روش میتوانیم به روشی مشابه با مثال زنجیرهای که در بالا داشتیم فکر کنیم. فقط در اینجا به جای زنجیره کردن توابع به یکدیگر، آرگومانها را به هم زنجیره میکنیم.
آرگومانهای پرانتز اول به تابع خارجی تعلق دارند، در حالی که آرگومانهای پرانتز دوم متعلق به تابع بازگشتی داخلی هستند.
همانطور که قبلا توضیح دادیم متد calculate()
یک تابع را برمیگرداند و آن همان تابع بازگشتی است که بلافاصله با استفاده از پرانتز دوم فراخوانی میشود.
یکی از جاهایی که میتوانیم این نوع نمادگذاری دو پرانتز را ببینیم، متد connect
در کتابخانه مدیریت state مربوط به redux است. در این لینک میتوانید بیشتر در مورد connect مطالعه کنید.