توابع Callback
پیش از این در مورد نحوهی ارسال آرگومانهای ورودی به توابع بحث کردیم و دیدیم که چطور میتوان انواع محتلف دادهها را به توابع به عنوان آرگومان ورودی ارسال کرد. در جاوا اسکریپت این امکان وجود دارد که یک تابع را به عنوان آرگومان ورودی به تابعی دیگر ارسال کرد. به تابعی که به عنوان آرگومان ورودی به تابعی دیگر ارسال میشود، تابع Callback یا Callback Function گفته میشود.
در واقع تفاوت توابع Callback با سایر توابع در نحوهی استفاده و کاربرد آنهاست. اما از نظر نحوهی تعریف، تفاوت خاصی وجود ندارد. حتی میتوان توابعی تعریف کرد که هم به صورت Callback و هم به صورت عادی به کار برده شوند. توابع Callback از مفاهیم بسیار مهم و پر کاربرد در جاوا اسکریپت به حساب میآیند و از این به بعد در این کتاب در موارد زیادی از این نوع توابع استفاده خواهیم کرد. در این بخش ابتدا به معرفی مفهوم توابع Callback و نحوهی استفاده از آنها میپردازیم. سپس با یک مثال، کاربرد عملی این توابع در مرتبسازی آرایهها نشان داده میشود. به قطعه کد زیر توجه کنید.
function print(){
console.log("I am print function.");
}
function example(callback){
callback();
}
example(print);
← "I am print function."
در مثال فوق ابتدا تابعی به نام print تعریف شده است که یک پیام ساده را در کنسول نمایش میدهد. سپس تابعی به نام example تعریف شده است که یک پارامتر ورودی به نام callback دارد. در خط ۶ میبینید که پارامتر callback همراه با یک جفت پرانتز به کار رفته است. این کار باعث میشود تا تابعی که در متغیر callback ذخیره شده است اجرا شود.
حال در خط ۹ تابع example فراخوانی شده و تابع print به عنوان آرگومان ورودی به این تابع ارسال شده است. در نتیجه تابعی که در خط ۶ اجرا میشود، در واقع همان تابع print است که خروجی آن را نیز مشاهده میکنید. توجه کنید که در هنگام ارسال توابع به عنوان آرگومان ورودی، فقط باید نام تابع را به کار برد و نباید از پرانتز استفاده کرد. این برنامه را میتوانید اینجا اجرا کنید.
در این مثال برای تعریف تابع Callback از روش Function Declaration استفاده شده است. اما میتوان از روش Function Expression نیز استفاده کرد. معادل مثال فوق با استفاده از Function Expression به این صورت خواهد بود.
const print = function (){
console.log("I am print function.");
}
function example(callback){
callback();
}
example(print);
← "I am print function."
همچنین میتوان Function Expression ها را به صورت توابع بینام در زمان فراخوانی توابع تعریف کرد. یعنی به جای قرار دادن نام یک تابع به عنوان آرگومان ورودی، خود تابع را در همان محل تعریف کرد. معادل مثال بالا با استفاده از توابع بینام به صورت زیر خواهد بود.
function example(callback){
callback();
}
example(function (){
console.log("I am print function.");
});
← "I am print function."
همانطور که میبینید به جای قرار دادن نام یک تابع به عنوان آرگومان ورودی تابع example، خود تابع را به صورت بینام (با Function Expression) قرار دادهایم. مزیت این روش کمتر شدن حجم کدنویسی است. اما دو ایراد نیز دارد. اولاً در صورت طولانی بودن بدنهی تابع، خوانایی برنامه کاهش مییابد. ثانیاً به دلیل بینام بودن تابع، نمیتوان از این تابع در مکان دیگری استفاده کرد. در صورتی که اگر تابع را قبلاً تعریف کرده باشیم، در هر نقطهای میتوان از آن تابع به صورت Callback یا عادی استفاده کرد.
همچنین میتوان توابع بینام را با Arrow Function ها ایجاد کرد. مثال فوق را میتوان با Arrow Function به صورت زیر اصلاح کرد.
function example(callback){
callback();
}
example( () => console.log("I am print function."));
← "I am print function."
میبینید که حجم کدها با استفاده از Arrow Function باز هم کمتر میشود. اما همانطور که قبلاً هم اشاره شده است، این روش مناسب توابعی است که کل بدنهی آنها شامل یک دستور باشد.
نکتهی مهمی که در زمان استفاده از توابع Callback باید به آن توجه کنید این است که اطمینان حاصل کنید که آرگومان ارسال شده حتماً یک تابع باشد. در غیر این صورت برنامه با خطا مواجه خواهد شد. به قطعه کد زیر توجه کنید.
function example(callback){
callback();
}
example(4);
← "TypeError: callback is not a function"
همانطور که مشاهده میکنید برنامهی فوق با خطا مواجه میشود. زیرا عدد ۴ که به عنوان ورودی به تابع example ارسال شده است یک تابع نیست. در نتیجه اجرای دستور خط ۲ امکانپذیر نیست. زیرا مقدار موجود در متغیر callback یک تابع نیست. برای حل مشکل فوق میتوان قبل از اجرای توابع callback، با استفاده از عملگر typeof از تابع بودن آنها مطمئن شد.
function example(callback){
if(typeof(callback) === 'function'){
callback();
}else{
console.log("پارامتر ورودی یک تابع نیست");
}
}
example(4);
← "پارامتر ورودی یک تابع نیست"
در کد فوق ابتدا در خط ۲ بررسی میشود که متغیر callback حتماً از نوع تابع (function) باشد. اگر این شرط برقرار باشد، تابع ذخیره شده در متغیر callback فراخوانی میشود. در غیر این صورت یک پیام خطا به کاربر نمایش داده میشود. توجه کنید که در اجرای هر دو مثال فوق کاربر با یک پیام خطا مواجه میشود. البته در مثال اول پیام خطا توسط مفسر جاوا اسکریپت تولید شده و برنامه را متوقف میکند. اما در مثال دوم، پیام خطا توسط برنامهنویس در نظر گرفته شده و فقط جهت اطلاعرسانی به کاربر است و اجرای برنامه را متوقف نمیکند.
اولین کاربرد توابع Callback
در فصل قبل با متد sort آشنا شدیم. این متد عناصر یک آرایه را مرتب میکند. همچنین دیدیم که این متد، مرتبسازی را بر اساس حروف الفبا انجام میدهد. در نتیجه اگر آرایهی مورد نظر حاوی عناصر عددی باشد، ابتدا این عناصر به رشته تبدیل شده، سپس بر اساس حروف الفبا مرتب میشوند. این رفتار در حالتی که هدف ما مرتبسازی بر اساس مقدار عددی باشد مشکلساز خواهد بود. به عنوان مثال در قطعه کد زیر عدد ۱۵ کوچکتر از عدد ۵ ارزیابی میشود. زیرا عدد ۱۵ با کاراکتر "1" شروع میشود که از نظر الفبایی از کاراکتر "5" کوچکتر است.
let numbers = [5 , 9 , 15 , 23];
numbers.sort();
← [15 , 23 , 5 , 9]
در فصل قبل نیز به این مشکل اشاره شد که در این بخش میخواهیم به نحوهی حل این مشکل با استفاده از توابع Callback بپردازیم. در واقع متد sort یک آرگومان ورودی اختیاری دارد که در صورت نیاز میتوان با ارسال یک تابع Callback به آن، نحوهی مرتبسازی عناصر آرایه را بر اساس منطق دلخواه تغییر داد.
تابعی که به متد sort ارسال میشود باید دو آرگومان ورودی دریافت کند. سپس این دو مقدار را بر اساس منطق دلخواه مقایسه کرده و بر اساس این مقایسه یکی از مقادیر زیر را بازگرداند.
- اگر آرگومان اول باید در آرایهی مرتب شده جلوتر قرار بگیرد، یک عدد منفی بازگرداند. (حالت کوچکتر بودن آرگومان اول)
- اگر آرگومان دوم باید در آرایهی مرتب شده جلوتر قرار بگیرد، یک عدد مثبت بازگرداند. (حالت بزرگتر بودن آرگومان اول)
- اگر تفاوتی در نحوهی قرارگیری دو آرگومان در آرایهی مرتب شده وجود ندارد، مقدار صفر را بازگرداند. (حالت تساوی دو آرگومان)
با ارسال چنین تابعی به متد sort، این متد برای مقایسهی عناصر آرایه، آنها را دو به دو به تابع Callback ارسال میکند. سپس بر اساس مقدار بازگشتی از این تابع، تصمیم میگیرد که کدام مقدار باید در آرایهی مرتب شده جلوتر قرار بگیرد. به عنوان مثال تابع مقایسه میتواند به صورت زیر نوشته شود.
function compare(value1 , value2){
if(value1 < value2){
return -1;
}else if(value1 > value2){
return 1;
}else{
return 0;
}
}
در تابع فوق، در صورتی که آرگومان اول از آرگومان دوم کوچکتر باشد (از نظر عددی)، مقدار 1- باز گردانده میشود. یعنی عددی که کوچکتر است باید جلوتر قرار بگیرد که این به معنی مرتبسازی صعودی است. اما اگر هدفمان مرتبسازی نزولی باشد، باید در این حالت مقدار 1+ را از تابع باز گردانیم. همچنین در صورت بزرگتر بودن آرگومان اول هم مقدار 1+ باز گردانده میشود. و در غیر این صورت (تساوی دو عدد)، مقدار صفر باز گردانده میشود.
حال میتوان یک آرایهی عددی را با استفاده از این تابع مقایسه، به راحتی به صورت صعودی مرتب کرد. برای این کار کافی است تابع compare به عنوان آرگومان به متد sort ارسال شود.
let numbers = [5 , 9 , 15 , 23 , 19];
numbers.sort(compare);
← [5 , 9 , 15 , 19 , 23]
این برنامه را میتوانید اینجا اجرا کنید. سعی کنید اعداد آرایه را تغییر داده و نتیجه را مشاهده کنید.
توجه کنید که برای تعریف تابع compare از روش Function Declaration استفاده شده است. اما میتوان از Function Expression یا Arrow Function نیز استفاده کرد. همچنین میتوان منطق به کار رفته در تابع compare را به شکل زیر ساده کرد.
function compare(value1 , value2){
return value1 - value2;
}
در این تابع نیز در صورت کوچکتر بودن آرگومان اول، مقدار بازگشتی منفی بوده، و در صورت بزرگتر بودن آرگومان اول مقدار بازگشتی مثبت خواهد بود. در صورت تساوی دو آرگومان نیز مقدار صفر بازگردانده میشود. همچنین با تغییر عبارت value1 - value2 به عبارت value2 - value1 میتوان علامت مقدار بازگشتی را تغییر داد. در نتیجه میتوان آرایه را به صورت نزولی مرتب کرد.
let compare = function(value1 , value2){
return value2 - value1;
}
let numbers = [5 , 9 , 15 , 23 , 19];
numbers.sort(compare);
← [23 , 19 , 15 , 9 , 5]
این برنامه را نیز میتوانید اینجا اجرا کنید.
همچنین میتوان جهت سادهسازی بیشتر، تابع مقایسه را به صورت یک تابع بدون نام در زمان فراخوانی متد sort تعریف کرد.
let numbers = [5 , 9 , 15 , 23 , 19];
numbers.sort(function(value1 , value2){
return value2 - value1;
});
← [23 , 19 , 15 , 9 , 5]
این برنامه نیز دقیقاً همان رفتار برنامهی قبلی را خواهد داشت. با این تفاوت که به دلیل بی نام بودن تابع مقایسه، امکان استفاده از این تابع در نقاط دیگر برنامه وجود ندارد. پس این روش فقط زمانی مناسب است که تابع مذکور فقط یک بار در برنامه مورد نیاز باشد.
همچنین برای سادهسازی هرچه بیشتر، میتوان از Arrow Function ها نیز استفاده کرد و مثال فوق را نیز به صورت زیر سادهتر کرد.
let numbers = [5 , 9 , 15 , 23 , 19];
numbers.sort((value1 , value2) => value2 - value1);
← [23 , 19 , 15 , 9 , 5]
پس در این بخش با مفهوم توابع Callback آشنا شدید و اولین مثال از کاربرد عملی توابع Callback را مشاهده کردید. در بخشهای بعدی نیز کاربردهای بیشتری از این نوع توابع را خواهید.