درک Reduce در جاوااسکریپت با ۵ مثال

ترجمه و تالیف : ابوالفضل باغشاهی
تاریخ انتشار : 22 تیر 99
خواندن در 3 دقیقه
دسته بندی ها : جاوا اسکریپت

متد ()reduce که در آرایه‌های جاوااسکریپت وجود دارد، یک فانکشن کاهنده (reducer) را روی هر کدام از اجزای آرایه‌ی مورد نظرتان، به اجرا در می‌آورد. این کار با استفاده از پاس دادن مقدار برگرداننده شده‌ از فراخوانی قبلی reducer به فراخوانی بعدی آن صورت می‌گیرد. فانکشن ()reduce بسیار باعث سردرگمی می‌شود؛ اما می‌تواند به خوانایی هر چه بیش‌تر کدهایتان کمک کند. مخصوصا زمانی که با دیگر مفاهیم برنامه‌نویسی کاربردی، ترکیب شود. در این‌جا قصد داریم ۴ مثال متداول و پر کاربرد از این فانکشن جاوااسکریپت را به همراه یک مثال کم کاربردتر از آن را بیاوریم تا به درک بهتری از فانکشن ()reduce در جاوااسکریپت برسید.

به‌دست آوردن مجموع مقادیر یک آرایه‌ی عددی

بیش‌تر آموزش‌های ()reduce با این مثال شروع می‌شوند: یک آرایه از اعداد داریم، [1, 3, 5, 7]، مجموع مقادیر آن را حساب کنید. ممکن است این عملیات جمع را با استفاده از حلقه‌ی for قدیمی و ساده‌ی جاوااسکریپت انجام دهید:

function sum(arr) {
  let sum = 0;
  for (const val of arr) {
    sum += val;
  }
  return sum;
}

sum([1, 3, 5, 7]); // 16

حال همان مثال را با استفاده از ()reduce حل کنیم:

function sum(arr) {
  const reducer = (sum, val) => sum + val;
  const initialValue = 0;
  return arr.reduce(reducer, initialValue);
}

sum([1, 3, 5, 7]); // 16

دو پارامتر اول فانکشن ()reduce، فانکشن ()reducer و initialValue (مقدار دلخواه اولیه) هستند. جاوااسکریپت ابتدا ()reducer را به همراه مقدار accumulator (انباشته‌گر) روی هرکدام از المنت‌های آرایه فراخوانی کرده. مقدار اولیه‌ی accumulator هم همان initialValue است. جاوااسکریپت از هر مقدار برگردانده شده از فراخوانی فانکشن ()reduce، به عنوان accumulator جدید استفاده می‌کند.

صحبت کافی است، بیاید کمی کد بزنیم. در زیر کد پیاده‌سازی شده‌ی یک کاهنده‌ی ساده را مشاهده می‌کنید:

function reduce(arr, reducer, initialValue) {
  let accumulator = initialValue;
  for (const val of array) {
    accumulator = reducer(accumulator, val);
  }
  return accumulator;
}

جمع کردن پراپرتی‌ (property)های عددی یک آرایه

در واقع فانکشن ()reduce بیش‌ از آن‌ که کاربردی باشد،‌ گیج کننده است. اگر تمام کاری که می‌خواهید با آن بکنید، جمع کردن المنت‌های یک آرایه باشد، احتمالا بهتر است از حلقه‌ی for استفاده کنید. امام وقتی فانکشن ()reduce را با دیگر متدهایی مانند ()filter و ()map استفاده کنید، ()reduce نیز کاربر خود را نشان خواهد داد و جذاب‌تر خواهد بود.

برای مثال تصور کنید که آرایه‌ای از اقلام یک فروشگاه را دارید و می‌خواهید مجموع پراپرتی property) total) همه‌ی این اقلام را حساب کنید.

const lineItems = [
  { description: 'Eggs (Dozen)', quantity: 1, price: 3, total: 3 },
  { description: 'Cheese', quantity: 0.5, price: 5, total: 2.5 },
  { description: 'Butter', quantity: 2, price: 6, total: 12 }
];

این یک روش برای جمع کردن totalهای این اقلام با استفاده از ()reduce است:

lineItems.reduce((sum, li) => sum + li.total, 0); // 17.5

این خط کد به خوبی کار می‌کند اما بهتر است از روش بهتر و سازنده‌تری استفاده کنیم. جایگزین بهتر این است که ابتدا با استفاده از ()map مقدار total را گرفته و سپس از آن در ()reduce استفاده کنیم:

lineItems.map(li => li.total).reduce((sum, val) => sum + val, 0);

چرا روش دوم بهتر است؟ چون که شما می‌توانید فانکشن مورد استفاده در ()reduce را جداگانه با نام ()sumReducer نوشته و در جاهای مختلف، در صورت نیاز، از آن استفاده کنید.

// Sum the totals
lineItems.map(li => li.total).reduce(sumReducer, 0);

// Sum the quantities using the same reducer
lineItems.map(li => li.quantity).reduce(sumReducer, 0);

function sumReducer(sum, val) {
  return sum + val;
}

این کار ممکن است به نظرتان بیهوده بیاید، اما به چند دلیل ایده‌ی بهتری برای انجام است؛ چرا که ممکن است همیشه مقدار نهایی ()sumReducer مطابق انتظار ما نباشد. یکی از این دلایل  این است که در کد بالا این واقعیت جاوااسکریپت که 0.1 + 0.2 !== 0.3 است، در نظر گرفته نشده است. این یک اشتباه رایج است که در هنگام محاسبه‌ی قیمت‌ها در زبان‌های تفسیر شده (interpretted) رخ می‌دهد. اعداد اعشاری دودویی عجیب و غریب هستند. پس با این تفاسیر، نیاز است که مقدار رند شده‌ را برگردانید:

const { round } = require('lodash');

function sumReducer(sum, val) {
  // Round to 2 decimal places.
  return _.round(sum + val, 2);
}

بدین ترتیب فانکشن ()reduce به خوبی در استفاده‌ی مجدد از منطق ()‌sumReducer با استفاده از زنجیره‌ی عملکردها (function chaining) به ما کمک می‌کند. در نهایت شما می‌توانید با تغییر یک‌باره‌ی منطق عملکردی که تعریف کرده‌اید، دیگر به دنبال تمام حلقه‌های for درون کدتان نگردید تا این منطق را یک به یک درون هر کدام، تغییر دهید.

پیدا کردن مقدار بیشینه (maximum)

در حالی که از ()reduce اغلب برای جمع کردن استفاده می‌شود، اما در راه‌های دیگری نیز می‌توان از این فانکشن استفاده کرد. مقدار accumulator را می‌توان روی هر مقداری که می‌خواهید، تنظیم کنید؛ عدد، null، undefined، promise، شی و آرایه جزو انواع داده‌ای هستند که می‌توان در accumulator جای‌گذاری کرد.

برای مثال تصور کنید که آرایه‌ای از تاریخ‌های جاوااسکریپتی دارید و می‌خواهید جدیدترین تاریخ را در میان آن‌ها پیدا کنید.

const dates = [
  '2019/06/01',
  '2018/06/01',
  '2019/09/01', // This is the most recent date, but how to find it?
  '2018/09/01'
].map(v => new Date(v));

یک رویکرد این است که این آرایه را مرتب (sort) کنید و آخرین المنت آن را برگردانید. قطعا این عملکرد کار خواهد کرد اما آیا واقعا این الگوریتم موثر است؟ چون مرتب کردن آرایه‌ای از تاریخ‌ها کار سبکی برای جاوااسکریپت نیست.

به جای این‌کار، می‌توانید از ()reduce استفاده کرده تا این فانکشن جدیدترین تاریخ موجود در آرایه را بیابد.

// This works because you can compare JavaScript dates using `>` and `<`.
// So `a > b` if and only if `a` is after `b`.
const maxDate = dates.reduce((max, d) => d > max ? d : max, dates[0]);

گروه‌بندی مقادیر

یک آرایه از اشیا (array of objects) داریم که هر کدام از اشیا آن، شامل پراپرتی age می‌شوند:

const characters = [
  { name: 'Jean-Luc Picard', age: 59 },
  { name: 'Will Riker', age: 29 },
  { name: 'Deanna Troi', age: 29 }
];

چطور می‌توانیم از آرایه‌ی بالا، جوابی را به‌دست آوریم که نشان دهنده‌ی تعداد افراد بر اساس سن‌ها باشد؟ مثلا جواب مورد نظر برای آرایه‌ی بالا { 29: 2, 59: 1 } است.

// Start with an empty object, increment `map[age]` for each element
// of the array.
const reducer = (map, val) => {
  if (map[val] == null) {
    map[val] = 1;
  } else {
    ++map[val];
  }
  return map;
};
characters.map(char => char.age).reduce(reducer, {});

زنجیره‌ی پرامیس‌ها (Promise Chaining)

در آخر تصور کنید که آرایه‌ای از فانکشن‌های ناهمزمان (async) داریم و می‌خواهیم که به شکل متوالی آن‌ها را اجرا کنیم. یک فانکشن غیر استاندارد با عنوان promise.series برای این منظور، وجود دارد؛ ولی این کار را می‌توانید با استفاده از ()reduce هم انجام دهید.

const functions = [
  async function() { return 1; },
  async function() { return 2; },
  async function() { return 3; }
];

// Chain the function calls in order, starting with an empty promise.
// In the end, `res` is equivalent to
// `Promise.resolve().then(fn1).then(fn2).then(fn3)`
const res = await functions.
  reduce((promise, fn) => promise.then(fn), Promise.resolve());
res; // 3

نتیجه‌گیری

فانکشن ()reduce یک ابزار پرقدرت است. با استفاده از مفاهیمی مانند کاهنده‌ها (reducers) و فیلترها (filters)، می‌توان تسک‌های رایجی مثل جمع کردن آرایه‌ای از اعداد را به سادگی با تقسیم آن عملکرد به فانکشن‌های کوچکت‌تر، انجام داد. در نهایت نیز می‌توان کدی تمیزتر که قابلیت refactoring آسان‌تری هم دارد، داشت.

منبع

گردآوری و تالیف ابوالفضل باغشاهی
آفلاین
user-avatar

Front-End

دیدگاه‌ها و پرسش‌ها

برای ارسال نظر لازم است ابتدا وارد سایت شوید
در حال دریافت نظرات از سرور، لطفا منتظر بمانید