אובייקט `Array`

אובייקט `Array`

מאמר זה מסביר על אובייקט ה-Array.

אסביר שימושים פרקטיים במערכים שלב אחר שלב בצורה ברורה וקלה להבנה.

YouTube Video

אובייקט Array

אובייקט ה-Array ב-JavaScript הוא אחד המבנים החשובים ביותר המהווים בסיס לכל סוגי עיבוד הנתונים. מפעולות בסיסיות במערכים ועד פונקציות מסדר גבוה, ישנם מאפיינים רבים שכדאי להכיר לשם עיבוד נתונים יעיל.

יסודות המערכים

ב-JavaScript, מערכים הם מבנה נתונים בסיסי לטיפול במספר ערכים יחדיו. כאן נציג כיצד ליצור מערכים, לקרוא ולכתוב לאלמנטים בהם באמצעות דוגמאות פשוטות.

 1// Create arrays in different ways
 2const arr1 = [1, 2, 3];           // array literal
 3const arr2 = new Array(4, 5, 6);  // Array constructor
 4const arr3 = Array.of(7, 8, 9);   // Array.of
 5
 6console.log("arr1 created with literal.   :", arr1);
 7console.log("arr2 created with constructor:", arr2);
 8console.log("arr3 created with Array.of.  :", arr3);
 9// arr1 created with literal.   : [ 1, 2, 3 ]
10// arr2 created with constructor: [ 4, 5, 6 ]
11// arr3 created with Array.of.  : [ 7, 8, 9 ]
12
13// Access and modify elements
14let first = arr1[0];              // read element
15console.log("First element of arr1:", first);
16// First element of arr1: 1
17
18arr1[1] = 20;                     // modify element
19console.log("arr1 after modifying index 1:", arr1);
20// arr1 after modifying index 1: [ 1, 20, 3 ]
21
22const len = arr1.length;          // get length
23console.log("Length of arr1:", len);
24// Length of arr1: 3
  • הקוד הזה מדגים שלוש דרכים ליצור מערכים, כיצד לקרוא ולעדכן ערכים באמצעות אינדקסים, וכיצד לקבל את האורך בעזרת המאפיין length.
  • מחרוזות מערך הן הנפוצות והקריאות ביותר, ומשתמשים בהן לרוב במצבים יומיומיים.

הוספה והסרה של אלמנטים (בקצה או בתחילת המערך)

מערכים מאפשרים להוסיף או להסיר אלמנטים בקלות בתחילת המערך או בסופו. פעולות אלו שימושיות במיוחד בעת מימוש מבנים כמו מחסנית (stack) או תור (queue).

 1// Push and pop (stack-like)
 2const stack = [];
 3console.log("Initial stack:", stack);
 4
 5stack.push(1);                    // push 1
 6console.log("After push(1):", stack);
 7
 8stack.push(2);                    // push 2
 9console.log("After push(2):", stack);
10
11const last = stack.pop();         // pop -> 2
12console.log("Popped value:", last);
13console.log("Stack after pop():", stack);
14
15// Unshift and shift (queue-like)
16const queue = [];
17console.log("Initial queue:", queue);
18
19queue.push('a');                  // add to end
20console.log("After push('a'):", queue);
21
22queue.unshift('start');           // add to front
23console.log("After unshift('start'):", queue);
24
25const firstItem = queue.shift();  // remove from front
26console.log("Shifted value:", firstItem);
27console.log("Queue after shift():", queue);
28
29// Initial stack: []
30// After push(1): [ 1 ]
31// After push(2): [ 1, 2 ]
32// Popped value: 2
33// Stack after pop(): [ 1 ]
34
35// Initial queue: []
36// After push('a'): [ 'a' ]
37// After unshift('start'): [ 'start', 'a' ]
38// Shifted value: start
39// Queue after shift(): [ 'a' ]
  • push ו-pop פועלות על סוף המערך. הן אידאליות למימוש מבנה מחסנית (stack).
  • unshift ו-shift פועלות על תחילת המערך. עם זאת, יש לשים לב שפעולה על תחילת המערך מחייבת הזזת כל האינדקסים פנימית, מה שהופך אותה לפעולה יקרה.

עבודה עם אלמנטים באמצע המערך (splice ו-slice)

כאשר מטפלים באלמנטים באמצע המערך, יש לבחור בין splice ל-slice לפי הצורך לשנות את המערך המקורי או לא. אם ברצונך רק לחלץ חלק ממערך, השתמש ב־slice; אם ברצונך לשנות את המערך עצמו, כגון הוספה או מחיקה של איברים, השתמש ב־splice.

 1// slice (non-destructive)
 2const nums = [0, 1, 2, 3, 4];
 3console.log("Original nums:", nums);
 4
 5const part = nums.slice(1, 4); // returns [1, 2, 3]
 6console.log("Result of nums.slice(1, 4):", part);
 7console.log("nums after slice (unchanged):", nums);
 8
 9// splice (destructive)
10const arr = [10, 20, 30, 40];
11console.log("\nOriginal arr:", arr);
12
13// remove 1 item at index 2, insert 25 and 27
14arr.splice(2, 1, 25, 27);
15console.log("After arr.splice(2, 1, 25, 27):", arr);
16
17// Original nums: [ 0, 1, 2, 3, 4 ]
18// Result of nums.slice(1, 4): [ 1, 2, 3 ]
19// nums after slice (unchanged): [ 0, 1, 2, 3, 4 ]
20
21// Original arr: [ 10, 20, 30, 40 ]
22// After arr.splice(2, 1, 25, 27): [ 10, 20, 25, 27, 40 ]
  • slice מחלץ אלמנטים בלבד ואינו משנה את המערך המקורי.
  • splice מוסיף או מסיר אלמנטים ומשנה את המערך עצמו, לכן יש להיזהר מהשפעתו על ההתנהגות.

איתורציה (for / for...of / forEach)

יש מספר דרכים לעבור ברצף על מערכים, ותוכל לבחור לפי הצורך וסגנון הקוד המועדף עליך. להלן שלוש תבניות איטרציה טיפוסיות.

 1const items = ['apple', 'banana', 'cherry'];
 2console.log("Items:", items);
 3
 4// classic for
 5console.log("\n--- Classic for loop ---");
 6for (let i = 0; i < items.length; i++) {
 7  console.log(`Index: ${i}, Value: ${items[i]}`);
 8}
 9
10// for...of (recommended for values)
11console.log("\n--- for...of loop ---");
12for (const item of items) {
13  console.log(`Value: ${item}`);
14}
15
16// forEach (functional style)
17console.log("\n--- forEach loop ---");
18items.forEach((item, index) => {
19  console.log(`Index: ${index}, Value: ${item}`);
20});
  • לולאת for היא הגמישה ביותר, ומאפשרת שליטה באינדקסים והפסקת איטרציה באמצעות break והוראות אחרות.
  • for...of מספקת דרך תמציתית לטפל בערכי אלמנטים והיא מאוזנת מאוד מבחינת קריאות הקוד.
  • forEach מאפשרת לכתוב קוד בסגנון פונקציונלי ומתאימה לפעולות תוצאת לוואי כמו תיעוד או עדכון נתונים על כל אלמנט. עם זאת, יש לזכור שלא ניתן להשתמש ב-break או continue, והיא אינה מתאימה לעיבוד אסינכרוני עם await.

map / filter / reduce — פונקציות מסדר גבוה

map, filter, ו-reduce הן פונקציות מסדר גבוה שנמצאות בשימוש תדיר בעת המרה, סינון או צבירה של מערכים. מכיוון שניתן לבטא תהליכים חוזרים בצורה ברורה, הקוד שלך הופך להיות פשוט וקל להבנה.

 1const numbers = [1, 2, 3, 4, 5];
 2console.log("Original numbers:", numbers);
 3
 4// map: transform each item
 5const doubled = numbers.map(n => n * 2);
 6console.log("\nResult of map (n * 2):", doubled);
 7
 8// filter: select items
 9const evens = numbers.filter(n => n % 2 === 0);
10console.log("Result of filter (even numbers):", evens);
11
12// reduce: accumulate to single value
13const sum = numbers.reduce((acc, n) => acc + n, 0);
14console.log("Result of reduce (sum):", sum);
15
16// Original numbers: [ 1, 2, 3, 4, 5 ]
17// Result of map (n * 2): [ 2, 4, 6, 8, 10 ]
18// Result of filter (even numbers): [ 2, 4 ]
19// Result of reduce (sum): 15
  • שיטות אלו מאפשרות להתמקד במה שרוצים לעשות בסגנון דקלרטיבי, משפרות קריאות ואף מסייעות להימנע מתופעות לוואי.

find / findIndex / some / every

להלן סקירה של שיטות חיפוש ובדיקת תנאים. שיטות אלו שימושיות למציאת אלמנטים שמקיימים תנאים מסוימים או לביצוע בדיקות בוליאניות על המערך.

 1const users = [
 2  { id: 1, name: 'Alice' },
 3  { id: 2, name: 'Bob' },
 4  { id: 3, name: 'Carol' }
 5];
 6
 7console.log("Users:", users);
 8
 9// Find the first user whose name is 'Bob'
10const bob = users.find(user => user.name === 'Bob');
11console.log("\nResult of find (name === 'Bob'):", bob);
12
13// Find index of the user whose id is 3
14const indexOfId3 = users.findIndex(user => user.id === 3);
15console.log("Result of findIndex (id === 3):", indexOfId3);
16
17// Check if there exists a user with id = 2
18const hasId2 = users.some(user => user.id === 2);
19console.log("Result of some (id === 2):", hasId2);
20
21// Check if all users have a numeric id
22const allHaveNumericId = users.every(user => typeof user.id === 'number');
23console.log("Result of every (id is number):", allHaveNumericId);
24// Result of find (name === 'Bob'): { id: 2, name: 'Bob' }
25// Result of findIndex (id === 3): 2
26// Result of some (id === 2): true
27// Result of every (id is number): true
  • find מחזירה את האלמנט הראשון התאמה לתנאי.
  • findIndex מחזירה את האינדקס של האלמנט שמקיים את התנאי.
  • some מחזירה true אם קיים לפחות אלמנט אחד שמקיים את התנאי.
  • every מחזירה true אם כל האלמנטים מקיימים את התנאי.

כולן מאוד שימושיות בטיפול במערכים, ושימוש נכון בהן לפי הצורך יהפוך את הקוד שלך לתמציתי וברור.

מיון ופונקציות השוואה

מערכים ניתנים למיון עם sort, אך כברירת מחדל היא משווה אלמנטים כמחרוזות, מה שעלול לגרום לתוצאות לא צפויות במספרים.

 1const nums = [10, 2, 33, 4];
 2console.log("Original nums:", nums);
 3
 4// Default sort: compares elements as strings (not suitable for numbers)
 5nums.sort();
 6console.log("\nAfter default sort (string comparison):", nums);
 7
 8// Numeric ascending sort using a compare function
 9nums.sort((a, b) => a - b);
10console.log("After numeric sort (a - b):", nums);
11
12// Sort objects by a property
13const people = [{ age: 30 }, { age: 20 }, { age: 25 }];
14console.log("\nOriginal people:", people);
15
16people.sort((a, b) => a.age - b.age);
17console.log("After sorting people by age:", people);
18
19// After default sort (string comparison): [ 10, 2, 33, 4 ]
20// After numeric sort (a - b): [ 2, 4, 10, 33 ]
21// Original people: [ { age: 30 }, { age: 20 }, { age: 25 } ]
22// After sorting people by age: [ { age: 20 }, { age: 25 }, { age: 30 } ]
  • בעת מיון מספרים או אובייקטים, יש לציין תמיד פונקציית השוואה כדי למיין כפי שהתכוונת.
  • בפונקציית השוואה, ערך שלילי מחזיר את a לפני b, ערך חיובי את b לפני a, ו-0 משאיר את הסדר ללא שינוי.

העתקת מערכים ובלתי-שינויאות (immutability)

בעת העתקת מערכים, חשוב להבין את ההבדל בין 'העתקת הפניה' לבין 'העתקה רדודה'. יש לשים לב במיוחד: אם יש אובייקטים בתוך המערך, העתקה רדודה תביא לשיתוף האובייקטים הפנימיים.

העתקת הפניה

כאשר משייכים מערך למשתנה אחר, המערך עצמו אינו משוכפל; במקום זאת, מועתקת 'הפניה' שמצביעה על אותו מערך.

 1const a = [1, 2, 3];
 2console.log("Original a:", a);
 3
 4// Reference copy; modifying b also affects a
 5const b = a;
 6console.log("\nReference copy b = a:", b);
 7// Reference copy b = a: [ 1, 2, 3 ]
 8
 9b[0] = 100;
10console.log("After modifying b[0] = 100:");
11console.log("a:", a);  // a: [ 100, 2, 3 ] (affected)
12console.log("b:", b);  // b: [ 100, 2, 3 ]
  • בהעתקת הפניה, אם תשנה את תכני המערך דרך המשתנה שהועתק, השינויים יתבטאו גם במשתנה המקורי שמפנה לאותו מערך.

העתקה רדודה

שימוש ב-slice() או בתחביר ה-spread יוצר 'העתקה רדודה', מכיוון שרק הערכים מועתקים; המערך המקורי והמערך המועתק מטופלים כישויות נפרדות.

 1const a = [1, 2, 3];
 2console.log("Original a:", a);
 3
 4// Shallow copy (new array)
 5const b = a.slice();
 6console.log("\nShallow copy using slice():", b);
 7// Shallow copy using slice(): [ 100, 2, 3 ]
 8
 9const c = [...a];
10console.log("Shallow copy using spread [...a]:", c);
11// Shallow copy using spread [...a]: [ 100, 2, 3 ]
12
13// Modifying c or d does NOT affect a
14b[1] = 200;
15c[2] = 300;
16console.log("\nAfter modifying b[1] = 200 and c[2] = 300:");
17console.log("a:", a);   // [ 100, 2, 3 ]
18console.log("b:", b);   // [ 100, 200, 3 ]
19console.log("c:", c);   // [ 100, 2, 300 ]
  • הקוד הזה ממחיש כי העתקה רדודה של מערך שנעשית בעזרת slice() או תחביר ה-spread לא משפיעה על המערך המקורי.

העתקה רדודה וחוסר השתנות (immutability)

גם אם משכפלים מערך בעזרת 'העתקה רדודה', ייתכן שיהיה שיתוף בלתי מכוון אם המערך מכיל אובייקטים בתוכו.

 1// Shallow copy doesn't clone inner objects
 2const nested = [{ x: 1 }, { x: 2 }];
 3console.log("\nOriginal nested:", nested);
 4// Original nested: [ { x: 1 }, { x: 2 } ]
 5
 6const shallow = nested.slice();
 7console.log("Shallow copy of nested:", shallow);
 8// Shallow copy of nested: [ { x: 1 }, { x: 2 } ]
 9
10// Changing inner object affects both arrays
11shallow[0].x = 99;
12console.log("\nAfter shallow[0].x = 99:");
13console.log("nested:", nested);
14console.log("shallow:", shallow);
15// nested: [ { x: 99 }, { x: 2 } ]
16// shallow: [ { x: 99 }, { x: 2 } ]
  • הקוד הזה ממחיש שבעת העתקה רדודה, האובייקטים הפנימיים משותפים, ולכן שינוי האובייקטים הללו ישפיע גם על המערך המקורי וגם על ההעתק.
  • אם יש צורך בנתונים עצמאיים, יש לבצע 'העתקה עמוקה' (deep copy) כמו באמצעות structuredClone() או המרה ל-JSON.

שיטות עזר שימושיות

להלן שיטות עזר נפוצות בעת עבודה עם מערכים. בשימוש נכון לצורך שלך, תוכל לכתוב קוד קצר וקריא יותר.

includes

שיטת includes בודקת האם ערך מסוים נמצא במערך.

1// includes: check if array contains a value
2const letters = ['a', 'b', 'c'];
3const hasB = letters.includes('b');
4console.log("letters:", letters);  // [ 'a', 'b', 'c' ]
5console.log("letters.includes('b'):", hasB); // true
  • בקוד הזה, שיטת includes משמשת כדי לבדוק בצורה תמציתית האם ערך מסוים קיים במערך.

concat

שיטת concat מחזירה מערך חדש שמצרף את המערך או הערכים שצוינו לסופו, תוך שמירה על המערך המקורי ללא שינוי.

1// concat: merge arrays without mutation
2const a1 = [1, 2];
3const a2 = [3, 4];
4const combined = a1.concat(a2);
5console.log("a1.concat(a2):", combined); // [ 1, 2, 3, 4 ]
6console.log("a1(unchanged):", a1);       // [ 1, 2 ]
  • הקוד הזה מדגים ש-concat אינה הרסנית, ומאפשרת ליצור מערך חדש תוך שמירה על המערך המקורי.

flat

באמצעות שיטת flat ניתן לשטח מערכים מקוננים.

1// flat and flatMap: flatten arrays or map + flatten in one step
2const nested = [1, [2, [3]]];
3console.log("nested:", nested); // nested: [ 1, [ 2, [ 3 ] ] ]
4console.log("nested.flat():", nested.flat()); // default depth = 1
5// nested.flat(): [ 1, 2, [ 3 ] ]
  • הקוד הזה מדגים את התוצאה של השטחת מערך ברמה אחת.
  • מכיוון ש-flat מאפשר לציין את עומק ההשטחה, ניתן לטפל במערכים מקוננים בהתאם לצורך בצורה גמישה.

flatMap

שיטת flatMap מבצעת שינוי על כל איבר ואז משטחת אוטומטית את התוצאות למערך חד-ממדי.

1// flat and flatMap: flatten arrays or map + flatten in one step
2const words = ['hello world', 'hi'];
3console.log("words:", words); // [ 'hello world', 'hi' ]
4
5const splitWords = words.flatMap(w => w.split(' '));
6console.log("words.flatMap(w => w.split(' ')):", splitWords);
7// words.flatMap(w => w.split(' ')): [ 'hello', 'world', 'hi' ]
  • הקוד הזה מדגים דוגמה שבה כל מחרוזת במערך מחולקת לפי רווחים, והתוצאות מחוברות ומשוטחות למערך יחיד.

join

שיטת join יוצרת מחרוזת על ידי חיבור איברי המערך עם מפריד שנבחר מראש.

1// join: combine elements into a string with a separator
2const csv = ['a', 'b', 'c'].join(',');
3console.log("['a', 'b', 'c'].join(','):", csv);  // a,b,c
  • בקוד הזה, שיטת join משמשת להמרת מערך למחרוזת בה האיברים מופרדים בפסיק.

טעויות נפוצות

עבודות עם מערכים עשויות להיראות פשוטות במבט ראשון, אך ישנן נקודות רבות העלולות להוביל להתנהגות בלתי רצויה. קל לפספס טעויות אלו בשימוש יומיומי במערכים, לכן חשוב לזכור את הנקודות הבאות לשם שיפור אמינות הקוד.

  • sort() של Array ממיינת לפי השוואת מחרוזות כברירת מחדל. בעת מיון מספרים כהלכה, חובה לספק תמיד פונקציית השוואה.
  • העתקת מערכים (באמצעות slice, אופרטור פריסה וכו') יוצרת העתקה רדודה. אם המערך שלך מכיל אובייקטים, עליך להיזהר - חלים שינויים על הנתונים המקוריים.
  • splice היא שיטה הרסנית שמבצעת שינוי ישיר במערך, בעוד ש-slice אינה משנה את המערך המקורי. חשוב להשתמש בהן בהתאם לצרכים שלך.
  • forEach אינה מתאימה ללולאות עם עיבוד אסינכרוני בעזרת await. אם ברצונך לבצע עיבוד אסינכרוני בסדר, מומלץ להשתמש ב-for...of.

דוגמה פרקטית

להלן דוגמה שמדגימה שימוש משולב בשיטות מערך כדי 'לקבל את סך כל הגילאים מנתוני משתמשים, לסנן את בני ה-30 ומעלה, וליצור רשימת שמות בלבד.'.

 1const users = [
 2  { name: 'Alice', age: 28 },
 3  { name: 'Bob', age: 34 },
 4  { name: 'Carol', age: 41 },
 5  { name: 'Dave', age: 19 }
 6];
 7
 8console.log("Users:", users);
 9
10// sum ages
11const totalAge = users.reduce((acc, u) => acc + u.age, 0);
12console.log("\nTotal age of all users:", totalAge);
13// Total age of all users: 122
14
15// filter and map names of users 30 and older
16const namesOver30 = users
17  .filter(u => u.age >= 30)
18  .map(u => u.name);
19
20console.log("Users aged 30 or older (names):", namesOver30);
21// Users aged 30 or older (names): [ 'Bob', 'Carol' ]
  • בעזרת שרשור (reduce, filter, map) ניתן לכתוב בצורה פשוטה קוד לצבירת ערכים, חילוץ לפי תנאי, ושינוי נתונים.
  • צינור עיבוד נתונים שכזה הוא קריא מאוד ומסייע למניעת תופעות לוואי, ולכן נפוץ מאוד בשימוש ביישומים אמיתיים.

סיכום

באמצעות מערכים ב-JavaScript, אפילו פעולות בסיסיות תקפות בהרבה תרחישים, ושימוש בפונקציות מסדר גבוה הופך את הקוד לתמציתי ואקספרסיבי יותר. יש לא מעט נקודות לזכור, אך כאשר תשלוט בשימוש הנכון בכל כלי, עיבוד הנתונים שלך ייהפך לזורם יותר.

תוכלו לעקוב אחר המאמר שלמעלה באמצעות Visual Studio Code בערוץ היוטיוב שלנו. נא לבדוק גם את ערוץ היוטיוב.

YouTube Video