آرایه ها در جاوااسکریپت
آرایه در جاوااسکریپت فقط یک ظرف داده نیست؛ زبان اصلی پردازش داده در فرانتاند است. از map و filter و reduce تا find و some و sort، همراه با الگوهای وا
سالها پیش، در یک پروژهی داشبورد، کدی به من رسید که برای محاسبهی مجموع فروش یک لیست سفارش، شصت خط حلقهی تودرتو داشت. کار میکرد، ولی هر بار که شرط تازهای اضافه میشد، یک if دیگر به آن اضافه میشد و خواندنش روزبهروز سختتر میشد. یک شب، با خودم گفتم ببینم نسخهی جدیدش چند خط میشود؛ با filter و reduce، به سه خط رسید — سه خطی که هم خواناتر بود و هم ده برابر سریعتر. آن شب برایم روشن شد که آرایهها در جاوااسکریپت فقط یک ساختار داده نیستند؛ زبان اصلی پردازش داده در فرانتاند هستند. اگر با این زبان آشنا نباشید، برای هر کار کوچکی سراغ حلقههای دستی میروید و کد شما، بهجای خوانا شدن، به یک درخت پیچیده تبدیل میشود. در این مقاله، همان مسیری را میروم که امروز با تازهکارها طی میکنم: از متدهای تغییرناپذیر و تجمیعی تا مرتبسازی، جستجو، الگوهای زنجیرهسازی و تلههایی که در پروژههای واقعی دیدهام.
آرایه در جاوااسکریپت: یک مرور سریع
اگر تازه با جاوااسکریپت آشنا میشوید، اول آموزش جاوااسکریپت از صفر را بخوانید و بعد مفاهیم پایه جاوااسکریپت را مرور کنید. حالا فرض کنیم با سینتکس آرایه و ایندکس آشنا هستید:
const fruits = ["apple", "banana", "cherry"];
console.log(fruits[0]); // apple
console.log(fruits.length); // 3
ولی در جاوااسکریپت مدرن، کار با آرایه بهندرت بهشکل دسترسی مستقیم با ایندکس انجام میشود. در عوض، از متدهای آرایه استفاده میکنیم — مجموعهای از توابع بومی که عملیاتهای رایج را به شکل امن، خوانا و سریع انجام میدهند. سه دلیل این ترجیح در پروژههای واقعی:
- خوانایی:
users.filter((u) => u.active)بسیار واضحتر از یک حلقهیforبا شرط داخلی است. - کاهش خطا: حلقههای دستی، منبع اصلی خطاهای آفبایوان و شرطهای اشتباه هستند. متدها این خطاها را از بین میبرند.
- کارایی: متدهای بومی در هستهی موتور جاوااسکریپت پیادهسازی شدهاند و معمولاً سریعتر از حلقههای دستی معادل هستند.
در جاوااسکریپت مدرن، اگر برای کاری حلقهی for مینویسید، احتمالاً یک متد آماده هست که همان کار را بهتر انجام میدهد — سراغش بروید.
چرا متدهای آرایه، ستون فقرات پردازش دادهاند؟
در پروژههای واقعی، سه سناریو پشت سر هم تکرار میشوند: دادهای از سرور میگیرید، آن را تغییر میدهید، و بخشی را نمایش میدهید. هر سه مرحله، با متدهای آرایه انجام میشوند. یک مثال ساده از یک اپلیکیشن فروشگاهی:
const orders = await fetch("/api/orders").then((r) => r.json());
// مرحله ۱: فیلتر سفارشهای تازه
const recent = orders.filter((o) => o.daysAgo <= 30);
// مرحله ۲: تبدیل هر سفارش به شکل نمایشی
const displayed = recent.map((o) => ({
id: o.id,
customer: o.customerName,
total: `${o.total.toLocaleString()} تومان`,
}));
// مرحله ۳: تجمیع مبلغ کل
const totalRevenue = recent.reduce((sum, o) => sum + o.total, 0);
همان سهگانهی filter → map → reduce را در پروژههای واقعی، بارها و بارها میبینید. تسلط بر این سه متد، بیش از ۷۰٪ کارهای پردازش داده را پوشش میدهد. اگر با آموزش ES6 در جاوااسکریپت آشنا هستید، میدانید که این متدها جزو ویژگیهای قدیمی جاوااسکریپت هستند (پیش از ES6)، ولی استفادهی مدرنشان — همراه با arrow function و zنجیرهسازی — از همان دوران ES6 به بعد رونق گرفت.
map: تبدیل هر عضو
map یک آرایهی جدید میسازد که هر عضو آن، نتیجهی اجرای یک تابع روی عضو متناظر آرایهی اصلی است. طول آرایهی خروجی، همیشه برابر با طول آرایهی ورودی است:
const numbers = [1, 2, 3, 4];
const doubled = numbers.map((n) => n * 2);
// [2, 4, 6, 8]
const users = [{ name: "Ali" }, { name: "Sara" }];
const names = users.map((u) => u.name);
// ["Ali", "Sara"]
سه نکتهی مهم در استفاده از map که در پروژههای واقعی به آنها رسیدهام:
- map همیشه آرایهی همطول برمیگرداند: حتی اگر تابع شما برای بعضی عناصر مقدار
undefinedبرگرداند، آن عنصر در خروجی هست. اگر میخواهید بعضی عناصر را حذف کنید، ازfilterاستفاده کنید. - map هرگز آرایهی اصلی را تغییر نمیدهد: این ویژگی، در پروژههای React و Vue بسیار مهم است — چون تغییرپذیری (mutation) باعث رفتار عجیب میشود.
- اگر تابع شما چیزی برنمیگرداند، map انتخاب اشتباهی است: در این حالت از
forEachاستفاده کنید. این اشتباه در پروژههای واقعی زیاد دیده میشود.
filter: انتخاب اعضا
filter آرایهی جدیدی میسازد که فقط شامل اعضایی است که تابع شرط شما برایشان true برگردانده:
const products = [
{ name: "Book", price: 120000, inStock: true },
{ name: "Pen", price: 20000, inStock: false },
{ name: "Notebook", price: 80000, inStock: true },
];
const available = products.filter((p) => p.inStock);
const affordable = products.filter((p) => p.price <= 100000);
const both = products.filter((p) => p.inStock && p.price <= 100000);
دو نکتهی مهم در filter که در پروژههای واقعی به آنها رسیدهام:
- تابع شرط باید boolean برگرداند: اگر تابع شما مقدار دیگری برگرداند، جاوااسکریپت خودش آن را به boolean تبدیل میکند — که گاهی منبع باگ است. مثلاً اگر یک مقدار
0برگردانید، بهfalseتبدیل میشود و عنصر حذف میشود. - ترکیب چند شرط با
&&و||: بهجای دو بار filter کردن، شرطها را در یک filter ترکیب کنید — چون هر filter یک پاس روی آرایه میزند.
reduce: تجمیع به یک مقدار
reduce آرایه را به یک مقدار واحد تبدیل میکند — مجموع، میانگین، آبجکت، یا هر چیز دیگر. این متد، قدرتمندترین و در عین حال گیجکنندهترین متد آرایه است:
const numbers = [1, 2, 3, 4, 5];
// مجموع
const sum = numbers.reduce((acc, n) => acc + n, 0);
// 15
// بیشترین
const max = numbers.reduce((acc, n) => (n > acc ? n : acc), 0);
// شمارش اعضا در دستهبندی
const items = ["a", "b", "a", "c", "a", "b"];
const counts = items.reduce((acc, item) => {
acc[item] = (acc[item] || 0) + 1;
return acc;
}, {});
// { a: 3, b: 2, c: 1 }
سه نکتهی مهم در استفاده از reduce که در پروژههای واقعی به آنها رسیدهام:
- همیشه مقدار اولیه بدهید: اگر مقدار اولیه ندهید، reduce از اولین عضو آرایه بهعنوان شروع استفاده میکند — که در آرایههای خالی، خطا میدهد. همیشه مقدار اولیه (مثل
0،{}یا[]) را مشخص کنید. - توابع پیچیده را به تابع جداگانه بکشید: وقتی reduce پیچیده میشود، خوانایی سقوط میکند. یک تابع نامدار بنویسید و همان را به reduce بدهید.
- reduce را برای همهچیز استفاده نکنید: گاهی ترکیب
filter + mapخواناتر از یک reduce پیچیده است. معیار من: اگر reduce شما بیش از سه خط منطق دارد، احتمالاً باید آن را بهfilter + map + reduceبشکنید.
reduce، چاقوی همهکارهی جاوااسکریپت است؛ چون همهکاره است، وسوسه میشوید همهجا از آن استفاده کنید — و همین، دام آن است. برای هر کاری متد مناسبتری هست.
find، findIndex، some، every
چهار متد که برای جستجو در آرایه استفاده میشوند، ولی هرکدام سناریوی خودشان را دارند:
| متد | خروجی | کاربرد |
|---|---|---|
find | اولین عضو منطبق یا undefined | پیدا کردن یک آیتم خاص |
findIndex | ایندکس اولین عضو منطبق یا -1 | نیاز به موقعیت |
some | true اگر حداقل یک عضو منطبق باشد | بررسی وجود |
every | true اگر همه منطبق باشند | بررسی عمومی |
const users = [
{ id: 1, name: "Ali", active: true },
{ id: 2, name: "Sara", active: false },
{ id: 3, name: "Reza", active: true },
];
const user = users.find((u) => u.id === 2); // { id: 2, ... }
const idx = users.findIndex((u) => u.id === 2); // 1
const hasInactive = users.some((u) => !u.active); // true
const allActive = users.every((u) => u.active); // false
نکتهی عملکردی مهم: some و every و find بهمحض پیدا کردن پاسخ، پیمایش را متوقف میکنند. یعنی در آرایههای بزرگ، بسیار سریعتر از filter(...).length > 0 هستند، چون filter کل آرایه را میپیماید. این تفاوت در پروژههای پربار، محسوس است — اصول کامل آن در بهینهسازی جاوااسکریپت آمده است.
forEach و تفاوتش با map
forEach برای پیمایش آرایه است، ولی هیچ مقداری برنمیگرداند:
const numbers = [1, 2, 3];
numbers.forEach((n) => {
console.log(n); // فقط چاپ میکند
});
// این اشتباه است — نتیجهای گرفته نمیشود
const doubled = numbers.forEach((n) => n * 2); // undefined
سه نکتهی مهم در استفاده از forEach که در پروژههای واقعی به آنها رسیدهام:
- برای کارهای جانبی، نه ساخت دادهی جدید: اگر هدف، جمعآوری نتیجه در یک آرایهی جدید است، از
mapاستفاده کنید نه forEach. - در forEach، await کار نمیکند: این یکی از بزرگترین تلههاست.
forEachمنتظرawaitنمیماند و کد شما ادامه مییابد. برای پیمایش async، ازfor...ofاستفاده کنید — اصول کامل در async/await در جاوااسکریپت آمده است. - نمیتوانید از forEach خارج شوید:
breakدر forEach کار نمیکند. اگر نیاز به توقف دارید، از حلقهیfor...ofاستفاده کنید یاsome/every.
sort و مرتبسازی پایدار
sort آرایه را در جای خود مرتب میکند (آرایهی اصلی را تغییر میدهد) و در نگاه اول ساده بهنظر میرسد، ولی چند تلهی رایج دارد:
const numbers = [10, 2, 30, 1];
// اشتباه — بهشکل رشته مرتب میکند
numbers.sort();
// [1, 10, 2, 30] — نه آنچه انتظار داشتید
// درست — با تابع مقایسهگر
numbers.sort((a, b) => a - b);
// [1, 2, 10, 30]
سه نکتهی مهم در sort که در پروژههای واقعی به آنها رسیدهام:
- بدون تابع مقایسهگر، رشتهای مرتب میکند: این رفتار شاید برای رشتهها درست باشد، ولی برای اعداد فاجعه است. همیشه برای اعداد، تابع مقایسهگر بدهید.
- sort در جای خود تغییر میدهد: اگر میخواهید آرایهی اصلی دستنخورده بماند، از
[...arr].sort(...)استفاده کنید — همان اصل immutability که در ادامه میآید. - sort پایدار است: از ES2019، sort در جاوااسکریپت پایدار است — یعنی عناصر با مقدار مساوی، ترتیب اصلی خود را حفظ میکنند. این ویژگی در پروژههای واقعی بسیار مفید است — مثلاً وقتی اول روی نام و بعد روی تاریخ مرتب میکنید.
مرتبسازی روی رشتههای فارسی، با localeCompare انجام میشود:
const names = ["رضا", "احمد", "زهرا"];
names.sort((a, b) => a.localeCompare(b, "fa"));
// ["احمد", "رضا", "زهرا"]
slice، splice و تفاوت کلیدی
دو متد با نامهای مشابه ولی رفتار کاملاً متفاوت:
const arr = [1, 2, 3, 4, 5];
// slice — کپی، بدون تغییر اصلی
const sliced = arr.slice(1, 3); // [2, 3]
console.log(arr); // [1, 2, 3, 4, 5]
// splice — تغییر در جا
const spliced = arr.splice(1, 2); // [2, 3]
console.log(arr); // [1, 4, 5]
تفاوت کلیدی: slice آرایهی اصلی را تغییر نمیدهد و یک کپی برمیگرداند؛ splice آرایهی اصلی را در جا تغییر میدهد. در پروژههای واقعی، من در ۹۰٪ موارد از slice استفاده میکنم — چون تغییرناپذیری (immutability) پایهی کار با React، Vue و Redux است.
علاوه بر این، splice میتواند برای افزودن هم بهکار برود:
const arr = [1, 2, 5];
arr.splice(2, 0, 3, 4); // در ایندکس ۲، بدون حذف، ۳ و ۴ را اضافه کن
console.log(arr); // [1, 2, 3, 4, 5]
flat، flatMap و آرایههای تودرتو
از ES2019، دو متد برای کار با آرایههای تودرتو اضافه شد:
const nested = [1, [2, 3], [4, [5, 6]]];
nested.flat(); // [1, 2, 3, 4, [5, 6]]
nested.flat(2); // [1, 2, 3, 4, 5, 6]
nested.flat(Infinity); // [1, 2, 3, 4, 5, 6]
// flatMap — map و flat با هم
const words = ["hello world", "good bye"];
const tokens = words.flatMap((w) => w.split(" "));
// ["hello", "world", "good", "bye"]
در پروژههای واقعی، flatMap بهویژه در پردازش دادههای تودرتو (مثل پاسخهای API با ساختار درختی) بسیار کاربرد دارد. اگر در fetch API در جاوااسکریپت با پاسخهای تودرتو کار کرده باشید، میدانید که flatMap چقدر میتواند کد را کوتاه کند.
تغییرناپذیری: چرا مهم است
یکی از مفاهیمی که در پروژههای واقعی، تفاوت بین کد قابلاعتماد و کد لرزان را میسازد، تغییرناپذیری (immutability) است. متدهایی مثل push، pop، shift، unshift، splice و sort آرایهی اصلی را تغییر میدهند؛ ولی متدهایی مثل map، filter، slice، concat و flat آرایهی جدید میسازند.
چرا این تفاوت مهم است؟ چون در پروژههای مدرن React، Vue و Redux، تغییرپذیری باعث میشود که سیستم تشخیص تغییر، از کار بیفتد — چون همان reference آرایه را میبیند و فرض میکند چیزی تغییر نکرده. این تله در پروژههای واقعی زیاد دیده میشود: کامپوننت بهروز نمیشود و توسعهدهنده ساعتها دنبال باگ میگردد.
// بد — تغییرپذیری
const users = [...];
users.push(newUser);
setUsers(users); // در React، ممکن است بهروزرسانی نکند
// خوب — تغییرناپذیری
const users = [...];
setUsers([...users, newUser]); // همیشه کار میکند
این موضوع در کار با DOM در جاوااسکریپت هم بهشکل دیگری ظاهر میشود — تغییرپذیری، رفتار غیرمنتظره در تشخیص تغییرات DOM ایجاد میکند.
الگوهای واقعی زنجیرهسازی
قدرت واقعی متدهای آرایه، در زنجیرهسازیشان است. چند الگوی پرکاربرد که در پروژههای واقعی به آنها رسیدهام:
الگوی اول: فیلتر و تبدیل
const displayed = users
.filter((u) => u.active)
.map((u) => ({
id: u.id,
label: `${u.firstName} ${u.lastName}`,
}))
.sort((a, b) => a.label.localeCompare(b.label, "fa"));
الگوی دوم: گروهبندی با reduce
const byCategory = products.reduce((acc, p) => {
const cat = p.category;
if (!acc[cat]) acc[cat] = [];
acc[cat].push(p);
return acc;
}, {});
الگوی سوم: جستجو و استخراج
const result = orders
.find((o) => o.id === targetId)
?.items
?.filter((i) => i.quantity > 1)
?.map((i) => i.productName) ?? [];
این الگو با optional chaining و nullish coalescing، در پروژههای واقعی به یک الگوی استاندارد تبدیل شده — چون از خطای Cannot read property جلوگیری میکند. اصول کامل optional chaining در آموزش ES6 در جاوااسکریپت آمده است.
الگوی چهارم: تحلیل داده با reduce
const stats = orders.reduce((acc, o) => ({
count: acc.count + 1,
total: acc.total + o.total,
max: Math.max(acc.max, o.total),
min: Math.min(acc.min, o.total),
}), { count: 0, total: 0, max: 0, min: Infinity });
کارایی: چه زمانی از چه متدی استفاده کنیم؟
در پروژههای واقعی، انتخاب متد درست، تفاوت بین کد سریع و کد کند است. چند قاعدهای که بهکار میبرم:
- بعضی کارها با some/every سریعتر از filter هستند: اگر فقط میخواهید وجود را بررسی کنید،
someدر اولین تطبیق متوقف میشود؛ ولیfilter(...).lengthکل آرایه را میپیماید. - زنجیرههای طولانی، چند بار روی آرایه پیمایش میکنند:
arr.filter().map().filter()سه بار روی آرایه پیمایش میکند. برای آرایههای بزرگ، یک reduce با منطق ترکیبی سریعتر است — هرچند خوانایی پایینتری دارد. - map + filter را در یک پاس ترکیب کنید: اگر پس از filter، map میزنید، همیشه میتوانید با reduce به یک پاس برسانید — ولی این بهینهسازی فقط زمانی ارزش دارد که آرایه بزرگ باشد یا در حلقه تکرار شود.
- find سریعتر از filter است برای اولین عضو:
filter(u => u.id === 5)[0]کل آرایه را میپیماید؛find(u => u.id === 5)در اولین تطبیق متوقف میشود.
در بهینهسازی، اولین سؤال این نیست «کدام متد سریعتر است؟» اولین سؤال این است «آیا این کد در مسیر بحرانی است؟» اگر نه، خوانایی همیشه برنده است.
اشتباهاتی که در پروژههای واقعی دیدهام
در بازبینی کد پروژههای مختلف، این اشتباهات را زیاد دیدهام:
- استفاده از forEach بهجای map: وقتی هدف، ساخت آرایهی جدید است،
forEachمقدار برنمیگرداند. در نتیجه، کدی مینویسند که باpushدر متغیر بیرونی کار میکند — پیچیده و ناامن. - await در forEach:
forEachمنتظرawaitنمیماند. این باگ در پروژههای واقعی زیاد دیده میشود و گاهی ساعتها دیباگ میطلبد. راهحل:for...ofیاPromise.all + map. - فراموش کردن تابع مقایسهگر در sort:
[10, 2, 30].sort()نتیجهی اشتباه میدهد چون بهشکل رشتهای مرتب میکند. - عدم توجه به تغییرپذیری: استفاده از
pushروی آرایهای که در state نگهداری میشود، در React بهروزرسانی را نمیزند. راهحل: استفاده از spread یا متدهای تغییرناپذیر. - استفاده از filter برای پیدا کردن یک عضو:
filter(...)[0]کندتر ازfindاست و اگر آرایه خالی باشد، مقدارundefinedمیدهد که ممکن است در ادامهی کد خطا بدهد. - reduce بدون مقدار اولیه:
arr.reduce((a, b) => a + b)روی آرایهی خالی، خطا میدهد. همیشه مقدار اولیه بدهید — حتی اگر0باشد. - نادیدهگرفتن خروجی sort روی رشتههای غیرلاتین: ترتیب حروف فارسی و ترکیبی از فارسی و انگلیسی، بدون
localeCompareنتیجهی اشتباه میدهد. - زنجیرهی بیپایان متدها:
filter + map + filter + map + sort + filterروی آرایهی بزرگ، چند برابر کندتر از یک reduce با منطق ترکیبی است. در آرایههای میلیونی، این تفاوت به ثانیهها میرسد. - استفاده از آرایه بهعنوان دیکشنری: در پروژههای واقعی، بعضی کدها از آرایه برای جستجو با شناسه استفاده میکنند (مثل
users.find(u => u.id === 5)در حلقه). راهحل: ساخت یکMapیا آبجکت برای جستجوی سریعتر — اصول کاملش در آموزش ES6 در جاوااسکریپت در بخش Map و Set آمده است. - بازنویسی حلقه با متد بدون تفکر: بعضی مواقع، حلقهی
forساده خواناتر از یک زنجیرهی پیچیده است. متد آرایه ابزار است، نه آیین.
یک توصیهی عملی از تجربه: در پروژههای جدید، قبل از نوشتن حلقهی for، دو سؤال از خودتان بپرسید: «آیا map، filter یا reduce این کار را بهتر انجام میدهد؟» و «آیا خروجی این کار باید آرایهی جدید باشد یا مقدار واحد؟» این دو سؤال، در ۹۰٪ موارد شما را به متد درست هدایت میکنند. اگر با ES6 آشنا نیستید، آموزش ES6 در جاوااسکریپت پیشنیاز خوبی است، و برای درک رفتار آرایهها در سطح عمیقتر، مفاهیم پایه جاوااسکریپت تفاوت کپی مقدار و مرجع را روشن میکند. اگر با TypeScript کار میکنید، آموزش تایپ اسکریپت از صفر نشان میدهد که چطور نوع آرایهها را دقیقتر تعریف کنید و از خطاهای زمان اجرا جلوگیری کنید.
سخن آخر
متدهای آرایه در جاوااسکریپت، از یک map ساده شروع میشوند ولی در پروژههای واقعی، به زبان اصلی پردازش داده تبدیل میشوند. سه نکتهی اصلی که در این مقاله به آنها رسیدیم: اول، سهگانهی filter → map → reduce بیش از ۷۰٪ کارهای پردازش داده را پوشش میدهد — تسلط بر همین سه، در پروژههای واقعی بیشترین بازده را دارد؛ دوم، تغییرناپذیری (immutability) پایهی کار با فریمورکهای مدرن است — متدهایی مثل map و filter و slice آرایهی اصلی را دستنخورده نگه میدارند، ولی push و splice و sort تغییر میدهند؛ سوم، انتخاب بین متدها باید آگاهانه باشد — some و find در اولین تطبیق متوقف میشوند، ولی filter کل آرایه را میپیماید؛ این تفاوت در آرایههای بزرگ محسوس است.
اگر امروز میخواهید در متدهای آرایه ماهر شوید، سه کار کوچک پیشنهاد میکنم: یک حلقهی for قدیمی از پروژهی خودتان را با filter + map بازنویسی کنید و تفاوت خوانایی را ببینید؛ یک آرایه از دادهی واقعی بگیرید و با reduce یک گزارش ساده (مجموع، میانگین، بیشترین) بسازید؛ و در یک پروژهی React یا Vue، جای push را با spread عوض کنید و ببینید که بهروزرسانی چطور درست میشود. همین سه تمرین، ۹۰٪ مهارتهای عملی کار با آرایه را در ذهن شما زنده میکند. مسیر طبیعی بعدی، شیگرایی در جاوااسکریپت، بهینهسازی جاوااسکریپت و async/await در جاوااسکریپت است — چون آرایه، ستون داده است و سهگانهی شیء، سرعت و آسنکرون، ادامهی طبیعی آن. اگر تجربهای از کار با متدهای آرایه در پروژههای خودتان دارید — مخصوصاً اگر با یک الگوی زنجیرهسازی جالب یا یک باگ تغییرپذیری روبرو شدهاید — در دیدگاهها بنویسید؛ همین نکتههای میدانی، برای خوانندهی بعدی از هر مستند رسمی ارزشمندتر است. 🧩