توسعه فرانت‌اند

Map, Filter, Reduce: قدرت توابع آرایه

قرارداد توابع آرایه، مقدار اولیه، کپی سطحی، داده نامعتبر و Callback async؛ نمونه کامل گزارش آموزشی با واحد پول و ورودی مشخص.

1404/12/17 2 دقیقه مطالعه 778 بازدید

map، filter و reduce برای سه کار متفاوت روی آرایه به کار می‌روند: تبدیل مقدارها، انتخاب آیتم‌ها و تجمیع نتیجه. انتخاب تابع باید از شکل خروجی بیاید. اگر به ازای هر آیتم یک خروجی می‌خواهید، map روشن است؛ اگر فقط بعضی آیتم‌ها لازم‌اند، filter شرط انتخاب را نشان می‌دهد. وقتی نتیجه یک مجموع یا ساختار تجمیعی است، reduce می‌تواند مناسب باشد. کوتاه کردن یک حلقه با هر کدام از این نام‌ها، به تنهایی خوانایی یا سرعت بیشتر نمی‌سازد.

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

map قرارداد تبدیل را بیان می‌کند

مرجع map در MDN ساخت آرایه نتیجه از Callback را توضیح می‌دهد. مقدار برگشتی Callback خروجی هر آیتم است. اگر هدف فقط تغییر DOM یا نوشتن Log است و خروجی map مصرف نمی‌شود، forEach یا حلقه می‌تواند نیت روشن‌تری داشته باشد. ساخت آرایه‌ای از undefined برای اثر جانبی، هزینه و ابهام اضافه دارد. در Callback دارای بلوک، return را فراموش نکنید؛ ظاهر Arrow Function به تنهایی مقدار مورد انتظار را برنمی‌گرداند.

آرایه جدید به معنی کپی عمیق همه objectها نیست. اگر Callback همان object ورودی را برگرداند، ارجاع آن همچنان مشترک است. تغییر property داخل map می‌تواند ورودی را هم تغییر دهد، حتی اگر Container آرایه تازه باشد. برای گزارش مستقل، object خروجی با فیلدهای لازم بسازید. Spread نیز کپی سطح اول است و object تودرتو ممکن است مشترک بماند. مقدار ثابت بودن نام آرایه با تغییرناپذیر بودن محتویات آن یکسان نیست.

filter شرط انتخاب را روشن می‌کند

راهنمای filter انتخاب آیتم‌ها بر اساس نتیجه Truthy شرط را توضیح می‌دهد. خروجی شامل همان آیتم‌های پذیرفته‌شده است و objectها خودکار Clone نمی‌شوند. شرط باید با معنی داده هماهنگ باشد. filter(Boolean) برای حذف مقدارهای Falsey کوتاه است، اما صفر یا رشته خالی شاید مقدار معتبر دامنه باشند. از کوتاهی شرط به جای تعریف داده استفاده نکنید؛ ممکن است اطلاعات درست بی‌صدا حذف شوند.

نام تابع شرط به خواندن گزارش کمک می‌کند. isPaidOrder روشن‌تر از یک شرط طولانی با چند وضعیت و حد عددی داخل زنجیره است، به شرط اینکه همان قواعد در تابع نوشته شده باشند. اگر شرط به تنظیمات بیرونی وابسته است، آن وابستگی مشخص باشد. تغییر زمان یا نرخ می‌تواند نتیجه را عوض کند. در گزارش تکرارپذیر، تنظیمات استفاده‌شده ثبت شوند تا دو اجرای مشابه با داده یکسان، بدون توضیح نتیجه متفاوت تولید نکنند.

reduce از مقدار اولیه شروع می‌کند

مرجع reduce نقش Accumulator و Initial Value را توضیح می‌دهد. برای مجموع عددی، صفر مقدار اولیه روشن است و آرایه خالی نتیجه قابل پیش‌بینی می‌دهد. حذف مقدار اولیه می‌تواند در آرایه خالی خطا ایجاد کند و نقش اولین آیتم را عوض کند. این تفاوت را با این جمله که reduce همیشه از صفر شروع می‌شود توضیح ندهید؛ مقدار شروع تابع باید در کد مشخص باشد.

نوع Accumulator باید با هدف سازگار بماند. شروع با رشته و اضافه کردن عدد، با جمع عددی یک نتیجه ندارد. object تجمیعی نیز باید هر بار به شکل مورد انتظار برگردد. Callbackی که یک شاخه‌اش return ندارد ممکن است در مرحله بعد Accumulator نامعلوم بدهد. اگر Reduce طولانی چند کار نامرتبط انجام می‌دهد، آن را به مرحله‌های نام‌دار تقسیم کنید. حلقه ساده برای بعضی تجمیع‌ها خواناتر است و استفاده از reduce معیار حرفه‌ای بودن کد نیست.

نمونه کامل گزارش داده آموزشی

فایل زیر را report.mjs ذخیره و با Node.js و فرمان node report.mjs اجرا کنید. package بیرونی، شبکه یا فایل داده جدا لازم ندارد. سه ردیف آموزشی تعریف شده‌اند؛ دو ردیف paid و یک ردیف cancelled است. قیمت‌ها با واحد صریح ریال و تعدادهای صحیح نوشته شده‌اند. این داده‌ها فرضی‌اند و نشان‌دهنده فروش یا عملکرد مشتری نیستند. نمونه مالی واقعی نیست و مالیات، تخفیف، برگشت وجه یا چند واحد پول را محاسبه نمی‌کند.

const orders=[
  {id:'demo-1',status:'paid',category:'نوشت‌افزار',quantity:2,unitPriceRial:50000},
  {id:'demo-2',status:'paid',category:'نوشت‌افزار',quantity:3,unitPriceRial:20000},
  {id:'demo-3',status:'cancelled',category:'لوازم',quantity:1,unitPriceRial:90000},
];
function validateOrder(order){return typeof order.id==='string'&&['paid','cancelled'].includes(order.status)&&typeof order.category==='string'&&Number.isSafeInteger(order.quantity)&&order.quantity>0&&Number.isSafeInteger(order.unitPriceRial)&&order.unitPriceRial>=0}
if(!orders.every(validateOrder))throw new Error('داده آموزشی نامعتبر است.');
const paid=orders.filter(order=>order.status==='paid');
const rows=paid.map(order=>({id:order.id,category:order.category,totalRial:order.quantity*order.unitPriceRial}));
if(!rows.every(row=>Number.isSafeInteger(row.totalRial)))throw new Error('مبلغ خارج از محدوده صحیح امن است.');
const totalRial=rows.reduce((sum,row)=>sum+row.totalRial,0);
if(!Number.isSafeInteger(totalRial))throw new Error('مجموع خارج از محدوده صحیح امن است.');
const byCategory=rows.reduce((groups,row)=>{groups.set(row.category,(groups.get(row.category)??0)+row.totalRial);return groups},new Map());
const emptyTotal=[].reduce((sum,row)=>sum+row.totalRial,0);
console.log({rows,totalRial,emptyTotal,groups:[...byCategory],original:orders});

filter دو ردیف paid را انتخاب می‌کند. map برای هر ردیف object خروجی با مبلغ ساخته و از تغییر داده اصلی پرهیز می‌کند. reduce مجموع مبلغ این دو ردیف را از صفر به دست می‌آورد: صد هزار و شصت هزار، در مجموع صدوشصت هزار ریال. مقدار برای توضیح همان داده آموزشی است و از هیچ گزارش واقعی استخراج نشده. اجرای reduce روی آرایه خالی با همان Initial Value صفر نتیجه می‌دهد و نیاز به شاخه خاصی برای جمع خالی ندارد.

بخش گروه‌بندی یک Map از دسته به مجموع می‌سازد. Accumulator در اینجا با نوع Map شروع می‌شود و همان ساختار برمی‌گردد. این استفاده از Map با تابع map آرایه یکی نیست؛ نام مشابه نباید نقش آن‌ها را مخلوط کند. تغییر ساختار Accumulator محلی در این نمونه، ورودی سفارش‌ها را تغییر نمی‌دهد. اگر گروه‌بندی با object معمولی انجام شود، درباره کلیدهای دلخواه و Prototype باید آگاهانه تصمیم بگیرید و نام دسته را داده معتبر فرض نکنید.

ورودی معتبر را جدا از تبدیل بررسی کنید

typeof number همه شرایط لازم را توضیح نمی‌دهد؛ NaN، Infinity و عدد خارج از محدوده ممکن است برای گزارش نامناسب باشند. برای تعداد، صحیح و مثبت بودن ممکن است لازم باشد. برای پول هم واحد و روش گرد کردن باید مشخص شود. تبدیل یک رشته با Number شرط تجاری را اجرا نمی‌کند. داده API را پیش از محاسبه بررسی کنید؛ مقاله TypeScript و مرز Runtime درباره تفاوت نوع اعلام‌شده و داده واقعی توضیح دارد.

اگر داده ناقص است، سیاست را ثبت کنید: رد کل گزارش، نمایش ردیف خطادار یا محاسبه بخشی با هشدار. حذف خاموش ردیف نامعتبر ممکن است مجموع ظاهراً دقیق اما ناقص بسازد. خطا نباید با صفر جایگزین شود مگر معنی واقعی دامنه همین باشد. برای داده سفارش، مبلغ صفر معتبر با مبلغ نامعلوم فرق دارد. قرارداد گزارش باید این تفاوت را نگه دارد تا خواننده نتیجه، نبود اطلاعات را با فروش صفر اشتباه نگیرد.

زنجیره خوانا را با هزینه واقعی بسنجید

filter و map معمولاً آرایه‌های میانی می‌سازند. برای داده کوچک، وضوح مرحله‌ها ممکن است ارزش بیشتری از حذف آن آرایه‌ها داشته باشد. برای داده بسیار بزرگ، تعداد عبور و تخصیص حافظه را بررسی کنید. تبدیل همه زنجیره‌ها به یک reduce پیچیده بدون شواهد، صرفاً هزینه خواندن کد را جابه‌جا می‌کند. هیچ نسبت سرعت عمومی برای برتری یکی از این روش‌ها اعلام نکنید؛ اندازه داده، Runtime و عملیات Callback در نتیجه اثر دارند.

Callback باید تا جای ممکن کار متناسب انجام دهد. دریافت شبکه یا ساخت DOM برای هر آیتم، هزینه‌ای متفاوت از جمع ساده دارد. انتقال یک تابع به Worker نیز خودکار بهترین انتخاب نیست؛ هزینه انتقال داده را بررسی کنید. مقاله Web Workers برای کار مستقل CPU زمینه دارد. در React، ساخت آرایه خروجی تازه می‌تواند روی Props اثر بگذارد؛ مقاله عملکرد React این رابطه را جدا از هزینه محاسبه توضیح می‌دهد.

Callback async نتیجه فوری داده نمی‌دهد

map با Callback async آرایه‌ای از Promiseها می‌سازد و خود آرایه منتظر همه نتیجه‌ها نمی‌ماند. باید سیاست انتظار و شکست را مشخص کنید. filter با Callback async نیز Boolean آینده را به شکل مورد انتظار مصرف نمی‌کند؛ Promise یک object Truthy است و می‌تواند همه آیتم‌ها را نگه دارد. برای انتخاب async، ابتدا نتیجه بررسی‌ها را دریافت و سپس انتخاب را انجام دهید. این تفاوت یکی از دلایل اهمیت شناخت قرارداد تابع آرایه است.

اجرای هم‌زمان تعداد زیاد درخواست می‌تواند فشار نامناسب ایجاد کند. Promise.all همه محدودیت‌های نرخ یا Retry را مدیریت نمی‌کند. اگر ترتیب و وابستگی مهم است، حلقه دارای await شاید قرارداد روشن‌تری داشته باشد؛ اگر مستقل‌اند، صف با هم‌زمانی محدود را بررسی کنید. مقاله Event Loop درباره تفاوت زمان‌بندی و اجرای واقعی توضیح می‌دهد. اسم تابع map نباید باعث شود عملیات شبکه به عنوان تبدیل فوری داده تحلیل شود.

آرایه Sparse و تغییر هنگام پیمایش را بشناسید

آرایه دارای Slot خالی با آرایه‌ای که مقدار undefined دارد یکسان نیست. بعضی روش‌ها Slotهای خالی را مانند مقدار معمولی فراخوانی نمی‌کنند و شکل خروجی می‌تواند فرق کند. داده معمول گزارش بهتر است آرایه متراکم و معتبر باشد. تغییر طول یا مقدارها هنگام Callback نیز استدلال را دشوار می‌کند. به جای اتکا به اثرهای جانبی پیمایش، ورودی و مرحله خروجی را جدا نگه دارید تا نتیجه برای مصرف‌کننده قابل پیش‌بینی بماند.

در تحویل، معنی هر مرحله و نوع نتیجه را بنویسید: انتخاب کدام آیتم، تبدیل به چه ساختار و تجمیع با چه مقدار شروع. واحد پول و سیاست داده نامعتبر جزئیات اضافه نیستند؛ بخشی از صحت نتیجه‌اند. مقاله نمایش جدول داده برای رساندن همین نتیجه به رابط مفید است. شروع مناسب یک گزارش کوچک با داده مشخص است؛ تابعی را انتخاب کنید که نیت همان مرحله را روشن کند و از زنجیره‌ای که فقط کوتاه‌تر به نظر می‌رسد پرهیز کنید.

واکنش به این مقاله

اشتراک‌گذاری این مقاله:
دیدگاه‌ها 0
۰ / ۲۰۰۰

هنوز دیدگاهی ثبت نشده

اولین نفری باشید که نظر می‌دهد!