`Array` object

This article explains about the Array object.

I will explain practical uses of arrays step-by-step in an easy-to-understand way.

YouTube Video

Array object

JavaScript's Array object is one of the most important structures forming the foundation of all kinds of data processing. From basic array operations to higher-order functions useful for efficient data transformation, there are many features you should know.

Array Basics

In JavaScript, arrays are a fundamental data structure for handling multiple values together. Here, we introduce how to create arrays and how to read and write their elements with simple examples.

 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
  • This code shows three ways to create arrays, how to read and update elements using indexes, and how to get the length using the length property.
  • Array literals are the most common and readable, and are used most frequently in everyday situations.

Adding and Removing Elements (at the End or Beginning)

Arrays allow you to easily add or remove elements at the end or beginning. These operations are also useful when implementing structures like stacks or queues.

 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 and pop operate on the end of an array. They are ideal for implementing stack structures.
  • unshift and shift operate on the beginning of an array. However, be aware that operating on the beginning requires shifting all element indexes internally, which makes it a costly operation.

Handling Elements in the Middle (splice and slice)

When handling elements in the middle of an array, choose between splice and slice depending on whether you want to modify the original array or not. If you just want to extract part of an array, use slice; if you want to modify the array itself, such as inserting or deleting elements, use 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 only extracts elements and does not modify the original array.
  • splice adds or removes elements and modifies the array itself, so be especially careful about its impact on behavior.

Iteration (for / for...of / forEach)

There are several ways to process arrays in sequence, and you can choose according to purpose and coding style preference. Here are three typical loop constructs.

 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});
  • The for loop is the most flexible, allowing for index operations and fine control over iteration using break and other statements.
  • for...of provides a concise way to handle element values and is the most balanced in terms of readability.
  • forEach allows for functional-style code and is well-suited to side-effect operations, like logging or updating data for each element. However, note that you cannot use break or continue, and it is not suitable for asynchronous processing with await.

map / filter / reduce — Higher-Order Functions

map, filter, and reduce are higher-order functions that are frequently used when transforming, filtering, or aggregating arrays. Since you can clearly express repetitive processing, your code becomes simple and easy to understand.

 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
  • These methods allow you to focus on what you want to do in a declarative style, improving readability and helping avoid unwanted side effects.

find / findIndex / some / every

Here is an overview of search and condition-checking methods. These are useful for finding elements that meet certain conditions or performing boolean checks on the set.

 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 returns the first element that matches the condition.
  • findIndex returns the index of the element that matches the condition.
  • some returns true if there is at least one element that satisfies the condition.
  • every returns true if all elements satisfy the condition.

All of these are very useful in array processing, so using them appropriately for the situation will keep your code concise and clear.

Sorting and Comparison Functions

Arrays are sorted using sort, but by default it compares elements as strings, which can lead to unexpected results when sorting numbers.

 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 } ]
  • When sorting numbers or objects, always specify a comparison function to sort them in the intended order.
  • In a comparison function, a negative return value places a before b, a positive puts b before a, and 0 keeps their order unchanged.

Array Copying and Immutability

When copying arrays, it's important to understand the difference between 'reference copy' and 'shallow copy.'. In particular, be aware that if there are objects inside the array, a shallow copy will cause the inner objects to be shared.

Reference copy

When you assign an array to another variable, the array itself is not duplicated; instead, the 'reference' pointing to the same array is copied.

 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 ]
  • With a reference copy, if you modify the contents of the array using the copied variable, those changes will also be reflected in the original variable that refers to the same array.

Shallow copy

Using slice() or the spread syntax creates a 'shallow copy' because only the values are duplicated; the original and copied arrays are treated as separate entities.

 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 ]
  • This code demonstrates that a shallow copy of an array created by slice() or the spread syntax does not affect the original array.

Shallow copy and immutability

Even if you duplicate an array using a 'shallow copy,' unintended sharing may occur if the array contains objects inside.

 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 } ]
  • This code demonstrates that with a shallow copy, the inner objects are shared, so modifying those inner objects affects both the original array and the copy.
  • If you need independent data, you need a 'deep copy' such as using structuredClone() or JSON conversion.

Useful utility methods

The following are utility methods frequently used when working with arrays. By using them appropriately for your purpose, you can write short and readable code.

includes

The includes method checks whether a specific value is included in an array.

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
  • In this code, the includes method is used to concisely determine whether a specified value exists within the array.

concat

The concat method returns a new array that appends the specified array or values to the end, while keeping the original array unchanged.

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 ]
  • This code shows that concat is non-destructive, allowing you to generate a new array while preserving the original one.

flat

Using the flat method, you can flatten nested arrays.

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 ] ]
  • This code demonstrates the result of flattening an array by one level.
  • Since flat lets you specify the depth, you can flexibly resolve nesting as needed.

flatMap

The flatMap method applies a transformation to each element and then automatically flattens the results into a one-dimensional array.

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' ]
  • This code shows an example where each string in an array is split by spaces, and the results are combined and flattened into a single array.

join

The join method creates a string by concatenating the elements of an array with a specified separator.

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
  • In this code, the join method is used to convert an array into a comma-separated string.

Common Pitfalls

Array operations may seem simple at first glance, but there are several points that can easily lead to unintended behavior. Many pitfalls are easy to miss during daily array operations, so keeping the following points in mind will greatly improve your code's reliability.

  • Array's sort() sorts by string comparison by default. When sorting numbers correctly, you must always provide a comparison function.
  • Copying arrays (via slice or spread syntax, etc.) creates a shallow copy. If your arrays contain objects, be careful because the original data may be unintentionally changed.
  • splice is a destructive method that directly changes the array, while slice is a non-destructive method that does not modify the original array. It is important to use them appropriately for your needs.
  • forEach is not suitable for loops with asynchronous processing using await. If you want to reliably execute asynchronous processing in order, it is recommended to use for...of.

Practical Example

Below is an example that combines array methods to 'get the total age from user data, extract those who are 30 or older, and create a list of names.'.

 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' ]
  • By chaining reduce, filter, and map, you can simply write aggregation, condition extraction, and transformation of data.
  • Such a 'data processing pipeline' is highly readable and, as a style with few side effects, is often used in real-world applications.

Summary

With JavaScript arrays, even basic operations are widely applicable, and by using higher-order functions, your code becomes more concise and expressive. There are many points to understand, but once you master how to use each appropriately, data processing will become much smoother.

You can follow along with the above article using Visual Studio Code on our YouTube channel. Please also check out the YouTube channel.

YouTube Video