אם אתם כותבים JavaScript וכבר נמאס לכם מלולאות for ארוכות בשביל פעולה פשוטה על מערך, הגיע הזמן להכיר לעומק את javascript array methods: בעיקר map, filter ו־reduce. אלה לא “קסמים” ולא קיצור מתחכם, אלא דרך קריאה ומדויקת יותר לתאר מה אתם רוצים לעשות עם נתונים.
במקום לנהל אינדקסים, ליצור מערך ריק, לדחוף אליו ערכים ולוודא שלא שכחתם תנאי, המתודות האלה מאפשרות לעבוד בשלבים ברורים: להפוך נתונים, לסנן נתונים, ולחשב מתוכם תוצאה אחת. במאמר הזה נכתוב קוד רץ, נראה פלט צפוי, ונעבור גם על טעויות נפוצות שמופיעות באמת אצל מתחילים.
אם אתם עדיין בתחילת הדרך בתכנות, כדאי לשמור בצד גם את מדריך צעדים ראשונים. ואם אתם מגיעים מרקע של פייתון, תזהו כאן רעיונות דומים לרשימות, סינון וצבירה, רק בתחביר של JavaScript.
מה צריך להכין מראש
כדי להריץ את הדוגמאות במאמר, מספיק שיהיה מותקן אצלכם Node.js. אפשר לבדוק זאת במסוף עם הפקודה node --version. אם מתקבלת גרסה כלשהי, אתם מסודרים. כל הדוגמאות כאן הן קבצי JavaScript רגילים שאפשר לשמור, למשל, בשם example.js ולהריץ עם node example.js.
אין צורך בספריות חיצוניות, אין צורך בבניית פרויקט, ואין צורך בדפדפן. המטרה היא להתמקד במערכים ובחשיבה שמאחורי המתודות.
map— מקבלת מערך ומחזירה מערך חדש באותו אורך, אחרי שינוי של כל איבר.filter— מקבלת מערך ומחזירה מערך חדש עם חלק מהאיברים, לפי תנאי.reduce— מקבלת מערך ומחזירה ערך אחד: מספר, אובייקט, מחרוזת, מערך חדש או כל מבנה אחר.
למה בכלל לא להשתמש בלולאת for?
לולאות הן כלי חשוב, ואין סיבה “להחרים” אותן. אבל בהרבה מקרים, לולאת for מערבבת כמה פעולות יחד: מעבר על המערך, בדיקת תנאי, יצירת תוצאה, עדכון משתנה עזר, ולפעמים גם שינוי של הנתונים המקוריים בטעות.
כאשר משתמשים במתודות מערך, הקוד מספר סיפור קצר יותר:
- “קח את כל המחירים והוסף מע״מ” —
map. - “השאר רק משתמשים פעילים” —
filter. - “חשב את הסכום הכולל” —
reduce.
היתרון המרכזי הוא קריאות. מפתח אחר שקורא את הקוד לא צריך לעקוב אחרי אינדקסים ומשתנים זמניים כדי להבין את הכוונה. הוא רואה את המתודה ומבין את הפעולה.
javascript array methods בפועל: שלושת הכלים שפותרים 80% מהמקרים
שלוש המתודות האלה מקבלות פונקציה כארגומנט. הפונקציה רצה על איברי המערך ומחזירה משהו. ההבדל ביניהן הוא מה JavaScript עושה עם הערך שחזר מהפונקציה.
| מתודה | מה היא מחזירה | מתי להשתמש |
|---|---|---|
map |
מערך חדש באותו אורך | כשכל איבר צריך להפוך למשהו אחר |
filter |
מערך חדש באורך משתנה | כשצריך להשאיר רק איברים שעומדים בתנאי |
reduce |
ערך אחד | כשצריך לצבור, לסכם, לקבץ או לבנות מבנה חדש |
map: להפוך כל איבר בלי לשנות את המערך המקורי
map מתאימה כשיש לכם מערך ואתם רוצים לקבל מערך חדש שבו כל איבר עבר המרה. למשל: מחירים לפני מע״מ למחירים אחרי מע״מ, שמות משתמשים לאובייקטים, או תאריכים לפורמט תצוגה.
const prices = [100, 250, 80];
const pricesWithVat = prices.map((price) => {
return price * 1.18;
});
console.log(prices);
console.log(pricesWithVat);הקוד יוצר מערך מחירים חדש בשם pricesWithVat. המערך המקורי prices לא משתנה. הפלט הצפוי הוא:
[ 100, 250, 80 ]
[ 118, 295, 94.39999999999999 ]שימו לב לתוצאה האחרונה. המספר 94.39999999999999 נובע מאופן הייצוג של מספרים עשרוניים במחשבים. אם מדובר בתצוגה למשתמש, אפשר לעגל:
const prices = [100, 250, 80];
const roundedPricesWithVat = prices.map((price) => {
return Math.round(price * 1.18);
});
console.log(roundedPricesWithVat);הפלט הצפוי:
[ 118, 295, 94 ]כאן השתמשנו ב־Math.round כדי לקבל מספרים שלמים. בדוגמה אמיתית של כסף, כדאי לחשוב מראש אם עובדים באגורות, בשקלים שלמים או בתצוגה בלבד, אבל העיקרון של map נשאר זהה.
filter: להשאיר רק את מה שעומד בתנאי
filter מתאימה למצבים שבהם יש מערך גדול יותר, ומתוכו רוצים רק חלק. הפונקציה שמועברת ל־filter חייבת להחזיר ערך שנחשב אמת או שקר. אם חזר ערך אמת, האיבר נשאר. אם חזר ערך שקר, האיבר יוצא מהתוצאה.
const users = [
{ name: "Dana", active: true, role: "admin" },
{ name: "Noam", active: false, role: "editor" },
{ name: "Maya", active: true, role: "editor" },
{ name: "Eli", active: true, role: "viewer" }
];
const activeEditors = users.filter((user) => {
return user.active === true && user.role === "editor";
});
console.log(activeEditors);הקוד עובר על כל המשתמשים ומשאיר רק משתמשים שהם גם פעילים וגם בעלי תפקיד editor. הפלט הצפוי:
[ { name: 'Maya', active: true, role: 'editor' } ]חשוב להבין ש־filter לא משנה את האובייקטים עצמם. היא רק בוחרת אילו איברים יופיעו במערך החדש. אם בתוך הפונקציה תשנו מאפיין של אובייקט, השינוי כן ישפיע על האובייקט המקורי, כי אובייקטים מועברים לפי הפניה. לכן מומלץ להשתמש ב־filter לבחירה בלבד, ולא לערבב בתוכו עדכון נתונים.
reduce: כשצריך תוצאה אחת
reduce היא המתודה שהכי מבלבלת בהתחלה, אבל גם אחת השימושיות ביותר. היא מקבלת פונקציה ושווי התחלתי. בכל סיבוב, הפונקציה מקבלת את התוצאה שנצברה עד עכשיו ואת האיבר הנוכחי, ומחזירה את התוצאה המעודכנת.
המקרה הפשוט ביותר הוא חיבור מספרים:
const numbers = [5, 10, 20];
const total = numbers.reduce((sum, number) => {
return sum + number;
}, 0);
console.log(total);הערך 0 בסוף הוא הערך ההתחלתי של sum. בכל מעבר נוסף אליו המספר הנוכחי. הפלט הצפוי:
35אבל reduce לא מוגבלת לסכומים. אפשר להשתמש בה כדי לקבץ נתונים לפי קטגוריה:
const products = [
{ name: "Keyboard", category: "hardware" },
{ name: "Mouse", category: "hardware" },
{ name: "Editor", category: "software" },
{ name: "Terminal", category: "software" }
];
const groupedByCategory = products.reduce((groups, product) => {
if (!groups[product.category]) {
groups[product.category] = [];
}
groups[product.category].push(product.name);
return groups;
}, {});
console.log(groupedByCategory);כאן הערך ההתחלתי הוא אובייקט ריק. בכל סיבוב בודקים אם כבר קיימת קטגוריה באובייקט. אם לא, יוצרים מערך חדש. לאחר מכן מוסיפים את שם המוצר למערך המתאים. הפלט הצפוי:
{
hardware: [ 'Keyboard', 'Mouse' ],
software: [ 'Editor', 'Terminal' ]
}שילוב map, filter ו־reduce בדוגמה אחת
בפועל, הרבה עבודה עם מערכים משלבת כמה שלבים. נניח שיש רשימת הזמנות, ורוצים לחשב את סכום ההזמנות ששולמו בלבד. אפשר לבצע זאת בצורה קריאה על ידי שרשור מתודות.
const orders = [
{ id: 1, paid: true, amount: 120 },
{ id: 2, paid: false, amount: 200 },
{ id: 3, paid: true, amount: 80 },
{ id: 4, paid: true, amount: 50 }
];
const paidTotal = orders
.filter((order) => {
return order.paid === true;
})
.map((order) => {
return order.amount;
})
.reduce((sum, amount) => {
return sum + amount;
}, 0);
console.log(paidTotal);השלבים ברורים: קודם משאירים רק הזמנות ששולמו, אחר כך לוקחים רק את הסכומים, ובסוף מחברים אותם. הפלט הצפוי:
250אפשר לכתוב את אותו דבר גם עם reduce בלבד, אבל לא תמיד זה עדיף. קוד קצר יותר אינו בהכרח קוד קריא יותר. כשאתם לומדים, העדיפו שרשור שמסביר את הכוונה בצורה ברורה.
מתי לא כדאי לשרשר יותר מדי
שרשור של map, filter ו־reduce יכול להיות אלגנטי, אבל גם להפוך לרכבת ארוכה וקשה לקריאה. אם השרשרת כוללת יותר משלושה או ארבעה שלבים, או אם בכל שלב יש פונקציה מורכבת, כדאי לעצור ולפרק למשתנים עם שמות טובים.
לדוגמה, במקום לכתוב הכול בשורה אחת, עדיף לפעמים ליצור:
paidOrders— הזמנות ששולמו.paidAmounts— הסכומים מתוך ההזמנות.paidTotal— הסכום הכולל.
הקוד אולי ארוך בכמה שורות, אבל יהיה קל יותר לבדוק אותו, להדפיס ערכי ביניים, ולתקן באגים. זה נכון במיוחד כשעובדים עם נתונים שמגיעים מקובץ, מטופס או מ־API.
טעויות נפוצות והודעות שגיאה אמיתיות
TypeError: numbers.map is not a function
השגיאה הזו מופיעה כשמנסים להפעיל map על משהו שאינו מערך.
const numbers = "5,10,20";
const doubled = numbers.map((number) => {
return number * 2;
});
console.log(doubled);ב־Node.js תקבלו הודעה בסגנון:
TypeError: numbers.map is not a functionהבעיה היא ש־numbers היא מחרוזת, לא מערך. תיקון אפשרי:
const numbers = "5,10,20".split(",").map((value) => {
return Number(value);
});
const doubled = numbers.map((number) => {
return number * 2;
});
console.log(doubled);הפלט הצפוי:
[ 10, 20, 40 ]TypeError: Reduce of empty array with no initial value
זו אחת הסיבות החשובות לתת תמיד ערך התחלתי ל־reduce. הקוד הבא נופל כי המערך ריק ואין ל־JavaScript ערך להתחיל ממנו:
const numbers = [];
const total = numbers.reduce((sum, number) => {
return sum + number;
});
console.log(total);השגיאה:
TypeError: Reduce of empty array with no initial valueהתיקון פשוט: מוסיפים ערך התחלתי.
const numbers = [];
const total = numbers.reduce((sum, number) => {
return sum + number;
}, 0);
console.log(total);הפלט הצפוי:
0מערך של undefined בגלל return חסר
זו לא תמיד שגיאת ריצה, ולכן היא מבלבלת במיוחד. כשמשתמשים בסוגריים מסולסלים בפונקציית חץ, חייבים לכתוב return.
const names = ["Dana", "Noam", "Maya"];
const greetings = names.map((name) => {
"Hello " + name;
});
console.log(greetings);הפלט יהיה:
[ undefined, undefined, undefined ]התיקון:
const names = ["Dana", "Noam", "Maya"];
const greetings = names.map((name) => {
return "Hello " + name;
});
console.log(greetings);הפלט הצפוי:
[ 'Hello Dana', 'Hello Noam', 'Hello Maya' ]איך לבחור בין map, filter ו־reduce
שאלה טובה לשאול היא: “איזו צורה תהיה לתוצאה?” אם התוצאה היא מערך באותו אורך, כנראה ש־map מתאימה. אם התוצאה היא מערך קצר יותר או שווה באורכו למקור, כנראה ש־filter מתאימה. אם התוצאה היא ערך אחד, לכו על reduce.
- רוצים לשנות כל איבר? השתמשו ב־
map. - רוצים לבחור חלק מהאיברים? השתמשו ב־
filter. - רוצים לחשב תוצאה אחת? השתמשו ב־
reduce. - רוצים רק לבצע פעולה צדדית כמו הדפסה? שקלו
forEach, אבל אל תשתמשו בו כדי לבנות מערך חדש.
אם אתם לומדים במקביל גם שפה אחרת, למשל דרך מדריכי פייתון, שימו לב להבדל בסגנון: ב־JavaScript מקובל מאוד לעבוד עם מתודות מערך ופונקציות חץ, בעוד שבפייתון תראו לעיתים list comprehensions או פונקציות כמו map ו־filter בגישה מעט אחרת.
תרגול קצר שכדאי לעשות עכשיו
כדי שהחומר יישב טוב, אל תסתפקו בקריאה. צרו קובץ בשם arrays-practice.js ונסו לפתור שלושה תרגילים קטנים:
- קבלו מערך של מספרים והחזירו מערך שבו כל מספר מוכפל ב־3.
- קבלו מערך של מוצרים והשאירו רק מוצרים שמחירם גדול מערך מסוים.
- קבלו מערך של ציונים וחשבו ממוצע בעזרת
reduce.
פתרון אפשרי לתרגיל הממוצע:
const grades = [90, 75, 100, 85];
const total = grades.reduce((sum, grade) => {
return sum + grade;
}, 0);
const average = total / grades.length;
console.log(average);הפלט הצפוי:
87.5אם המערך עלול להיות ריק, צריך לבדוק זאת לפני חילוק ב־grades.length, אחרת תקבלו תוצאה לא שימושית כמו NaN. זה מסוג הפרטים הקטנים שמבדילים בין דוגמה לימודית לבין קוד שיכול להתמודד עם קלט אמיתי.
סיכום: פחות רעש, יותר כוונה בקוד
map, filter ו־reduce הן לא רק קיצורי דרך. הן דרך לכתוב קוד שמבטא כוונה: המרה, סינון וצבירה. ברגע שמתרגלים לחשוב כך, הרבה קטעי קוד שהיו נכתבים בעבר עם לולאות מסורבלות הופכים לפשוטים יותר לקריאה ולבדיקה.
הכלל המעשי הוא להתחיל מהשאלה מה אתם רוצים לקבל בסוף. מערך חדש באותו אורך? map. מערך מסונן? filter. תוצאה אחת? reduce. משם כבר קל יותר לבנות את הפתרון צעד אחר צעד, בלי להסתבך עם אינדקסים ומשתני עזר מיותרים.


