متدهای ()map() ،reduce و ()filter در جاوا اسکریپت — به زبان ساده
اگر به تازگی شروع به یادگیری جاوا اسکریپت نمودهاید، شاید تاکنون اسامی ()map() ،reduce و ()filter به گوشتان نخورده باشد. شاید کسانی که مجبور بودهاند پشتیبانی از Internet Explorer 8 را در پروژه خود داشته باشند با این واژهها ناآشنا باشند. اما اگر نیازی به ایجاد سازگاری با یک چنین مرورگر قدیمی نداشته باشید، میبایست با این متدها آشنا شوید.
دقت کنید که مفاهیمی که در این مقاله ارائه میشوند، احتمالاً در زبانهای برنامهنویسی دیگر نیز به کار شما میآیند، زیرا این مفاهیم در میان زبانهای زیادی مشترک هستند.
()map.
عملکرد این متد را با یک مثال تشریح میکنیم.
فرض کنید یک آرایه شامل چند شیء دریافت کردهاید که هر یک از آنها نماینده یک شخص هستند. با این حال شما در انتها به یک آرایه شامل تنها id هر شخص نیاز دارید:
// What you have var officers = [ { id: 20, name: 'Captain Piett' }, { id: 24, name: 'General Veers' }, { id: 56, name: 'Admiral Ozzel' }, { id: 88, name: 'Commander Jerjerrod' } ]; // What you need [20, 24, 56, 88]
چندین روش برای رسیدن به این آرایه وجود دارد. ممکن است با ایجاد یک آرایه خالی و سپس استفاده از (forEach() ،.for(...of یا یک ()for. ساده به مقصود خود برسید.
هر کدام از این روشها را مقایسه میکنیم:
استفاده از ()forEach.
var officersIds = []; officers.forEach(function (officer) { officersIds.push(officer.id); });
دقت کنید که از قبل یک آرایه خالی ایجاد میکنیم. حال ببینیم موقع استفاده از ()map. چه رخ میدهد؟
var officersIds = officers.map(function (officer) { return officer.id });
با استفاده از تابعهای Arrow (که نیازمند پشتیبانی از ES6، Babel یا TypeScript است)، حتی میتوانیم خلاصهتر از این عمل کنیم:
const officersIds = officers.map(officer => officer.id);
بنابراین دیدیم که ()map. چگونه عمل میکند. اساساً ما دو آرگومان داریم که یکی callback و دیگری یک چارچوب اختیاری است (که در این callback به صورت this تصور میشود). اما ما در مثال قبلی از آن استفاده نکردیم. Callback برای هر مقدار در آرایه فعال میشود و همه مقدارهای جدید را در آرایه حاصل بازمیگرداند.
به خاطر داشته باشید که آرایه حاصل همواره طولی برابر با آرایه اصلی دارد.
()reduce.
این متد نیز دقیقاً همانند ()map. و ()reduce. یک callback برای هر عنصر آرایه بازمیگرداند. تنها تفاوت این است که در اینجا ارسال نتیجه این callback (یعنی accumulator) از یک عنصر به عنصر دیگر صورت نمیگیرد.
Accumulator میتواند هر چیزی باشد (عدد صحیحی، رشته، شیء و غیره) و باید هنگام فراخوانی ()reduce. یک وهله از آن ایجاد یا ارسال شود.
اینک نوبت به بررسی یک مثال رسیده است. فرض کنید آرایهای داریم که در آن اسامی خلبانها و سنوات خدمتشان آمده است:
var pilots = [ { id: 10, name: "Poe Dameron", years: 14, }, { id: 2, name: "Temmin 'Snap' Wexley", years: 30, }, { id: 41, name: "Tallissan Lintra", years: 16, }, { id: 99, name: "Ello Asty", years: 22, } ];
میخواهیم کل سالهای سنوات خدماتی همه خلبانها را بدانیم. این کار با استفاده از متد ()reduce. کاملاً سرراست است:
var totalYears = pilots.reduce(function (accumulator, pilot) { return accumulator + pilot.years; }, 0);
دقت کنید که مقدار آغازین به صورت 0 تعیین شده است. در صورت نیاز میتوان از یک متغیر موجود نیز استفاده کرد. پس از اجرای callback برای هر عنصر آرایه، متد reduce مقدار نهایی accumulator ما را که در این مورد برابر با 82 است بازمیگرداند. در قطعه کد زیر نسخه فشرده کد فوق با استفاده از تابعهای arrow در ES6 را میبینید:
const totalYears = pilots.reduce((acc، pilot) => acc + pilot.years، 0);
اینک فرض کنید میخواهم بدانیم کدام خلبان از همه باتجربهتر است. بدین منظور نیز میتوانیم از reduce استفاده کنیم:
var mostExpPilot = pilots.reduce(function (oldest, pilot) { return (oldest.years || 0) > pilot.years ? oldest : pilot; }, {});
ما accumulator خود را oldest مینامیم. Callback ما اقدام به مقایسه accumulator هر خلبان میکند. اگر خلبانی سالهای خدمتی بالاتر از متغیر oldest داشته باشد، در این صورت آن خلبان به oldest جدید تبدیل میشود و این روند همین طور تا آخر تداوم مییابد.
همان طور که شاهد هستید استفاده از ()reduce. روشی آسان برای تولید مقدار منفرد یا شیء از یک آرایه محسوب میشود.
()filter.
چه میشود اگر آرایهای داشته باشیم که تنها به برخی از عناصر آن نیاز داشته باشیم؟ همین جا است که ()filter. به کار میآید. دادههای ما به صورت زیر هستند:
var pilots = [ { id: 2, name: "Wedge Antilles", faction: "Rebels", }, { id: 8, name: "Ciena Ree", faction: "Empire", }, { id: 40, name: "Iden Versio", faction: "Empire", }, { id: 66, name: "Thane Kyrell", faction: "Rebels", } ];
فرض کنید میخواهیم دو آرایه داشته باشیم که یکی برای خلبانهای تازهکار و دیگری برای خلبانهای کهنهکار است. انجام این کار با استفاده از ()filter. بسیار آسان است:
var rebels = pilots.filter(function (pilot) { return pilot.faction === "Rebels"; }); var empire = pilots.filter(function (pilot) { return pilot.faction === "Empire"; });
به همین سادگی این کار را انجام دادیم. با استفاده از تابعهای Arrow از این هم سادهتر میشود:
const rebels = pilots.filter(pilot => pilot.faction === "Rebels"); const empire = pilots.filter(pilot => pilot.faction === "Empire");
اگر تابع callback مقدار true بازمیگرداند، عنصر کنونی در آرایه حاصل خواهد بود؛ اما اگر مقدار بازگشتی false باشد نخواهد بود.
ترکیب ()map() ، .reduce. و ()filter.
از آنجا که هر سه این متدها روی آرایه استفاده میشوند و ()map. و ()filter. مقدار آرایهای بازمیگردانند، ما میتوانیم فراخوانیهای خود را به سادگی زنجیرهسازی کنیم. در ادامه مثال دیگری را بررسی میکنیم. دادههای ما چنین است:
var personnel = [ { id: 5, name: "Luke Skywalker", pilotingScore: 98, shootingScore: 56, isForceUser: true, }, { id: 82, name: "Sabine Wren", pilotingScore: 73, shootingScore: 99, isForceUser: false, }, { id: 22, name: "Zeb Orellios", pilotingScore: 20, shootingScore: 59, isForceUser: false, }, { id: 15, name: "Ezra Bridger", pilotingScore: 43, shootingScore: 67, isForceUser: true, }, { id: 11, name: "Caleb Dume", pilotingScore: 71, shootingScore: 85, isForceUser: true, }, ];
هدف ما این است که امتیاز کلی نیروهای نظامی را به دست آوریم. این کار را گام به گام اجرا میکنیم. ابتدا باید کارکنانی که نمیتوانند در نیروی نظامی استفاده شوند را حذف کنیم:
var jediPersonnel = personnel.filter(function (person) { return person.isForceUser; }); // Result: [{...}, {...}, {...}] (Luke, Ezra and Caleb)
بدین ترتیب سه عنصر در آرایه حاصل باقی میماند. ما باید یک آرایه شامل امتیاز کلی هر نیرو ایجاد کنیم:
var jediScores = jediPersonnel.map(function (jedi) { return jedi.pilotingScore + jedi.shootingScore; }); // Result: [154, 110, 156]
همچنین از reduce برای دریافت امتیاز کلی استفاده میکنیم:
var totalJediScore = jediScores.reduce(function (acc, score) { return acc + score; }, 0); // Result: 420
اکنون به بخش جذاب ماجرا میرسیم. ما میتوانیم همه این موارد را به صورت زنجیرهای در یک خط کد اجرا کنیم:
var totalJediScore = personnel .filter(function (person) { return person.isForceUser; }) .map(function (jedi) { return jedi.pilotingScore + jedi.shootingScore; }) .reduce(function (acc, score) { return acc + score; }, 0);
با استفاده از تابعهای arrow بسیار جذابتر به نظر میرسد:
const totalJediScore = personnel .filter(person => person.isForceUser) .map(jedi => jedi.pilotingScore + jedi.shootingScore) .reduce((acc, score) => acc + score, 0);
دقت کنید که در مثال قبلی ()map. و ()filter. ضروری نبودند. ما میتوانیم به سادگی همین نتیجه را تنها با ()reduce. به دست آوریم. ما آنها را صرفاً به منظور ارائه مثال آنجا قرار دادهایم. آیا میتوانید حدس بزنید که چگونه صرفاً با حفظ ()reduce. میتوانیم همین نتیجه را با تنها یک خط کد به دست آوریم؟
چرا از ()forEach. استفاده نکنیم؟
اغلب برنامهنویسان معمولاً همه جا از حلقههای for به جای ()map() ،reduce و ()filter استفاده میکنند. اما اگر تجربه برنامهنویسی با دادههای مرتبط با API را داشته باشید، میتوانید مزیت کنار گذاشتن forEach را به خوبی متوجه شوید.
قالببندی
فرض کنید میخواهیم فهرستی از افراد را با نام و عنوان شغلیشان نشان دهیم:
var data = [ { name: "Jan Dodonna", title: "General", }, { name: "Gial Ackbar", title: "Admiral", }, ]
API دادههای فوق را در اختیار ما قرار میدهد؛ اما ما صرفاً به عنوان و نام خانوادگی هر فرد نیاز داریم. بنابراین باید دادهها را قالببندی کنیم. با این وجود اپلیکیشن ما باید یک نمای منفرد از هر فرد نیز داشته باشد و از این رو میبایست یک تابع قالببندی داده داشته باشیم که هم نمای فهرستوار و هم نمای منفرد را ارائه کند.
این بدان معنی است که نمیتوانیم حلقه foreach. را درون تابع قالببندی داشته باشیم، چون در این صورت باید عنصر منفرد خود را پیش از ارسال به تابع، درون پوششی به صورت زیر قرار دهیم تا کار کند:
var result = formatElement([element])[0]; // Yeah... that's not right at all
بنابراین حلقه باید فراخوانی تابع را به صورت زیر پوشش دهد:
data.forEach(function (element) { var formatted = formatElement(element); // But what then.... });
اما ()forEach. هیچ چیزی بازنمیگرداند. این بدان معنی است که باید نتایج را درون یک آرایه از پیش تعیین شده ارسال کنید.
var results = []; data.forEach(function (element) { var formatted = formatElement(element); results.push(formatted); });
در نتیجه باید 2 تابع داشته باشیم: تابع ()formatElement و تابع خودمان که نتایج را به آرایه ارسال میکند. شاید از خود بپرسید که وقتی میتوانیم تنها یک تابع داشته باشیم، چه نیازی به 2 تابع داریم؟
var results = data.map(formatElement);
تست کردن آسانتر است
اگر تستهای unit برای کد خود مینویسید، متوجه خواهید شد که تست کردن تابعهایی که با ()mapو ()reduce و ()filter فراخوانی میشوند آسانتر است.
تنها کاری که باید انجام دهیم این است که دادهها را برای تابع آمادهسازی کنیم و منتظر باشیم تا خروجی آن را دریافت کنیم. در واقع ما صرفاً this را ارسال میکنیم و منتظر خروجی میمانیم. بدین ترتیب به دستکاری کمتر، و همچنین ()beforeEach-ها و ()afterEach-های کمتری نیاز داریم و تست کردن آسان و سرراست خواهد بود.
سعی کنید در کدهایتان برخی از حلقههای for را در مواردی که متناسب است با ()map() ،reduce و ()filter جایگزین کنید. مطمئن باشید که کد شما یکپارچهتر شده و خوانایی آن افزایش مییابد.
اگر این مطلب برایتان مفید بوده است، آموزشهای زیر نیز به شما پیشنهاد میشوند:
- مجموعه آموزشهای برنامهنویسی
- آموزش جاوا اسکریپت (JavaScript)
- مجموعه آموزشهای طراحی و برنامه نویسی وب
- آموزش کاربردی برنامه نویسی وب با جی کوئری
- ایجاد ارتباط بین پایتون و جاوا اسکریپت با JSON
==