کار با آرایهها در JavaScript دیگر به ندرت به معنای نیاز به نوشتن یک حلقه for است. این زبان اکنون ابزارهای بیانگری در اختیار شما قرار میدهد که دادههای تودرتو و پر از سر و صدا را به تغییرات تمیز و خوانا تبدیل میکنند. این راهنما از جایی که مبانی تمام میشوند ادامه مییابد—با تمرکز بر تخت کردن (flattening)، کاهش دادن (reducing)، پرسوجو (querying) و الگوهای جدید تغییرناپذیر (immutable) که دادههای اصلی شما را دستنخورده نگه میدارند.
تخت کردن ساختارهای تودرتو با flat() و flatMap()
دادههای دنیای واقعی عاشق تودرتو بودن هستند. وقتی رکوردها را از یک API میگیرید، یک فایل CSV را تجزیه میکنید یا یک فایل پیکربندی را میخوانید، اغلب با آرایههایی درون آرایههای دیگر مواجه میشوید. متد flat() برای تبدیل آن لایهها به یک لیست خطی واحد وجود دارد.
متد flat() را روی یک آرایه تودرتو فراخوانی کنید؛ به صورت پیشفرض، فقط یک سطح را تخت میکند:
const nested = [[1, 2], [3, 4]];
const flat = nested.flat();
// [1, 2, 3, 4]
آرایه اصلی nested دقیقاً همانطور که بود باقی میماند. اگر تودرتویی شما عمیقتر است، یک آرگومان عمق مانند flat(2) یا حتی flat(Infinity) را در صورتی که نمیدانید چند لایه وجود دارد، پاس دهید. این کار بسیار بهتر از نوشتن دستی توابع کمکی بازگشتی (recursive) است.
زمانی که نیاز دارید عناصر را تغییر دهید و نتیجه را نیز تخت کنید، flatMap() هر دو کار را در یک مرحله انجام میدهد. این متد یک تابع نگاشت (mapping function) را روی هر عنصر اجرا کرده و سپس آرایههای بازگشتی را یک سطح تخت میکند. انجام این کار با فراخوانیهای جداگانه .map().flat() کار میکند، اما flatMap() از ایجاد آرایه میانی جلوگیری میکند که باعث صرفهجویی در حافظه و جلوگیری از یک تکرار (iteration) اضافی میشود. یک مورد استفاده کلاسیک، تقسیم یک آرایه از جملات به کلمات مجزا است:
const lines = ['hello world', 'foo bar'];
const words = lines.flatMap(line => line.split(' '));
// ['hello', 'world', 'foo', 'bar']
فیلتر کردن و نگاشت: تفاوت را بدانید
متد filter() یک آرایه جدید شامل مواردی که از تست شما سربلند بیرون میآیند (pass) ایجاد میکند. تابع بازگشتی (callback) باید یک مقدار truthy برای نگه داشتن عنصر، یا یک مقدار falsy برای حذف آن برگرداند. این متد هرگز آرایه منبع را تغییر نمیدهد (mutate).
const users = [
{ name: 'Ajay', active: true },
{ name: 'Bina', active: false }
];
const activeUsers = users.filter(u => u.active);
اشتباه گرفتن filter() با map() آسان است، زیرا هر دو آرایههای جدیدی برمیگردانند و callback میپذیرند. تفاوت در قرارداد آنهاست. متد map() یک آرایه جدید با همان طول برمیگرداند. اگر هر عنصر را تغییر دهید و یک بررسی شرطی انجام دهید، در واقع برای هر جایگاه یک true یا false خواهید دید، زیرا map() هرگز آیتمها را حذف نمیکند—فقط آنها را تغییر میدهد. از سوی دیگر، filter() یک آرایه احتمالاً کوتاهتر شامل فقط عناصری که از تست شما عبور کردهاند را برمیگرداند. اگر میخواهید دادهها را حذف کنید، از filter() استفاده کنید. اگر میخواهید هر قطعه از داده را تغییر دهید در حالی که تعداد آنها را حفظ میکنید، از map() استفاده کنید.
فشرده کردن آرایهها با reduce()
در حالی که map() و filter() آرایه برمیگردانند، reduce() کل یک لیست را به یک مقدار واحد تقلیل میدهد. این متد از چپ به راست حرکت کرده و یک انباشتگر (accumulator) را در هر مرحله با خود حمل میکند.
const totals = [10, 20, 30];
const sum = totals.reduce((acc, current) => acc + current, 0);
// 60
آرگومان دوم—که در مثال بالا 0 است—مقدار اولیه انباشتگر است. اگر آن را نادیده بگیرید، JavaScript اولین عنصر را به عنوان نقطه شروع در نظر میگیرد و تکرار را از ایندکس ۱ آغاز میکند. این موضوع زمانی که آرایه خالی باشد باعث سردرگمی کاربران میشود، زیرا فراخوانی reduce() بدون مقدار اولیه روی یک آرایه خالی، خطای TypeError میدهد.
reduce فقط برای محاسبات ریاضی نیست. میتوانید از آن برای ساخت جداول فراوانی، ادغام اشیاء، یافتن مقادیر حداکثر یا عبور دادن دادهها از چندین مرحله تغییر استفاده کنید. از آنجایی که بسیار منعطف است، بهتر است تابع callback را کوچک نگه دارید و متغیرهای خود را به وضوح نامگذاری کنید. یک بلوک reduce() متراکم به سرعت سختتر از حلقهای که جایگزین کرده است، خوانده میشود.
پرسوجوهای بولی: every() و some()
گاهی اوقات شما به یک آرایه جدید نیاز ندارید؛ بلکه به یک پاسخ بله یا خیر درباره مجموعه نیاز دارید. every() تنها در صورتی true برمیگرداند که همه عناصر شرط شما را برآورده کنند. اگر اولین عنصر شکست بخورد، بلافاصله متوقف شده و false برمیگرداند.
const scores = [85, 92, 78];
const allPassed = scores.every(s => s >= 50);
متد some() تصویر آینهای آن است. اگر حداقل یک عنصر از تست عبور کند، true برمیگرداند. به محض یافتن یک مورد مطابقت، عملیات را در همان لحظه متوقف میکند (short-circuits).
const hasPerfectScore = scores.some(s => s === 100);
این متدها جایگزین حلقههای دستی برای بررسیهای اعتبارسنجی (validation) میشوند. نیاز دارید تأیید کنید که تمام فیلدهای فرم پر شدهاند؟ از every() استفاده کنید. نیاز دارید بدانید آیا هیچ کاربری دسترسی ادمین دارد؟ از some() استفاده کنید.
بررسی و بهروزرسانی بدون تغییر دادن منبع (Mutation)
جاوااسکریپت مدرن به شدت به سمت تغییرناپذیری (immutability) متمایل است. این متدها به شما کمک میکنند تا آرایهها را بدون دست زدن به نسخه اصلی، بررسی یا بهروزرسانی کنید.
متد keys() یک شیء iterator شامل ایندکسهای آرایه برمیگرداند. entries() یک iterator از جفتهای [index, value] برمیگرداند. اینها در حلقههای for...of زمانی که میخواهید از ساختارشکنی (destructuring) تمیز استفاده کنید، میدرخشند:
const fruits = ['apple', 'banana'];
for (const [index, fruit] of fruits.entries()) {
console.log(index, fruit);
}
with() یک افزودهی جدیدتر به این زبان است که عنصری را در یک ایندکس مشخص جایگزین کرده و یک آرایه کاملاً جدید برمیگرداند. این متد همان نیازِ تخصیص با استفاده از براکت — arr[index] = value — را برطرف میکند، با این تفاوت که آرایه اصلی را دستنخورده باقی میگذارد. در فریمورکهایی مانند React، که در آنها تغییر مستقیم (mutation) باعث از کار افتادن تشخیص تغییرات (change detection) میشود، استفاده از with() مسیر امنتری است:
const original = [10, 20, 30];
const updated = original.with(1, 99);
// original: [10, 20, 30]
// updated: [10, 99, 30]
Spread و Rest: دو جهتِ ...
نحو (syntax) spread و پارامتر rest هر دو از سه نقطه یکسان استفاده میکنند، اما دادهها را در جهتهای مخالف جابهجا میکنند.
از اپراتور spread برای تبدیل یک آرایه به عناصر مجزا استفاده کنید. این تمیزترین راه برای ادغام یا کپی کردن آرایهها است:
const front = [1, 2];
const back = [3, 4];
const merged = [...front, ...back];
const copy = [...merged];
همچنین میتوانید به راحتی موارد اضافی را هم در میان آنها قرار دهید: [0, ...front, 99, ...back].
پارامتر rest چندین عنصر را در یک آرایه واحد جمعآوری میکند. در داخل امضای یک تابع (function signature)، این پارامتر هر آنچه از آرگومانها باقی مانده باشد را جمعآوری میکند:
function logFirst(first, ...rest) {
console.log('First:', first);
console.log('Rest:', rest);
}
logFirst('a', 'b', 'c');
// First: a
// Rest: ['b', 'c']
این دو در کنار هم وظیفه رایجِ پذیرش ورودیهای متغیر (variadic input)، پردازش آنها به عنوان یک آرایه، و سپس پخش کردن مجدد آنها در فراخوانیهای تابع یا ساختارهای داده جدید را انجام میدهند.
نکته اصلی
هدف از این متدها، تحت تأثیر قرار دادن همتیمیها با کدهای تکخطی نیست؛ بلکه هدف، آشکار کردن نیت شماست. وقتی توسعهدهنده دیگری filter() را میبیند، میداند که در حال حذف کردن آیتمها هستید. وقتی reduce() را میبیند، میداند که در حال فشردهسازی دادهها هستید. وقتی with() را میبیند، میداند که آرایه اصلی در جایی دستنخورده باقی مانده است. تسلط بر این ابزارها به معنای نوشتن کدی است که خودش را توضیح میدهد، و این کار بسیار بیشتر از هر حلقه (loop) هوشمندانهای در زمان صرفهجویی میکند.
