Array Methods in JavaScript

Photo by Capturing the Human Heart on Unsplash

Common and useful methods on JavaScript's Array prototype


A few months ago, I gave a Lunch and Learn talk at work about some common methods and patterns for working with arrays in JavaScript. I had noticed some common confusion among some of the junior engineers on my team when working with collections of data, so I decided to collect some patterns and present them to team at large.

This post is a written adaptation of that talk. The original format for that talk was a walk through of the README and JavaScript files located in this repo. There's also a quiz to test your array knowledge!

Intro - What are arrays?

If you're new to programming or JavaScript, you might not be completely sure what an array is. An array is the term that JavaScript uses for a list or collection of data. They look like this:

const fruits = ['Strawberry', 'Banana', 'Apple'];

Dealing with arrays is very common in JavaScript development. Whether you're working with data from a REST API endpoint or the DOM, it's very likely that you'll often be working with collections of values or objects.

Native Array Methods

JavaScript arrays have a bunch of native methods defined on the Array prototype. Because these methods are defined on the Array prototype, any Array will have access to these methods. This is the same for properties defined on the prototype, like .length.

const myFriends = ['Jane', 'John', 'Joe'];

// Array.prototype.length
console.log(myFriends.length); // 3

Having a working knowledge of these native methods is great for improving your effectiveness and productivity with JavaScript. I'd definitely suggest reading over the Array docs from MDN, and taking a look at all the different array methods and examples there.

Why learn these methods?

Availability

These methods are always available to you. They're like a tool belt that comes for free whenever you're working in a JavaScript environment. JavaScript might not have a standard library (yet), but it does provide lots of methods natively on its various prototypes like Array and Object.

Declarative

JavaScript's native array methods are highly declarative as opposed to imperative. They tell the computer what to do, rather than how to do it. Because of this, complex chains of logic can be represented more succinctly or coherently with these methods as opposed to a big chain of nested for loops and if statements. That's not to say that for loops and if statements don't have their places, though!

forEach()

The first method we're going to take a look at is .forEach(). From the MDN docs

The forEach() method executes a provided function once for each array element.

So, the forEach() method is called on any array, and accepts a callback function that will receive each element as its first argument. It looks like this, in practice:

const toys = ['Truck', 'Doll', 'Ball'];

toys.forEach((toy) => {
  console.log(`You're getting a ${toy} for your birthday this year!`);
});

// Output:
// You're getting a Truck for your birthday this year!
// You're getting a Doll for your birthday this year!
// You're getting a Ball for your birthday this year!

The most common use case for forEach() is executing some code for each element of an array where the result of that execution either doesn't exist or doesn't matter. In our example above, we're outputting a string of text to the console, but we're not returning any data or calculating any new values based on the array's elements.

forEach() is also useful for mutating the original array. Mutations can sound a little scary, but sometimes they are necessary or even preferred. For instance, adding a new property to each object in an array of objects is a good use case for forEach():

const users = [
  { id: 1, name: "Steph"},
  { id: 2, name: "Steve}
]

users.forEach(user => user.username = user.name + user.id)

console.log(users)

// Output:
// [
//   { id: 1, name: 'Steph', username: 'Steph1' },
//   { id: 2, name: 'Steve', username: 'Steve2' }
// ]

As a learning exercise, let's re-implement each array method as we go. We'll keep things simple, and we won't handle edge cases or optional parameters like the real JavaScript methods do for simplicity's sake. Here's what our own forEach() might look like:

function myForEach(array, callback) {
  for (let i = 0; i < array.length; i++) {
    callback(array[i]);
  }
}

const sides = ['Fries', 'Chips', 'Salad'];
myForEach(sides, (side) => console.log(side));

// Output:
// Fries
// Chips
// Salad

At their core, a lot of these native array methods are syntactic sugar for common operation involving for loops like this. By creating named methods and accepting a callback, JavaScript can provide a robust, declarative interface for working with arrays, rather than leaving everything up to the user to implement imperatively.

map()

map() is a method used for deriving a new array from another array. From MDN

The map() method creates a new array with the results of calling a provided function on every element in the calling array.

map() is most commonly used for transforming array data into a new structure, or running some sort of calculation on every element of an array. For instance, let's try doubling each number in an array with map():

const numbers = [1, 2, 3, 4];
const doubledNumbers = numbers.map((number) => number * 2);

console.log(doubledNumbers); // [2, 4, 6, 8]
console.log(numbers); // [1, 2, 3, 4]

Notice how the original numbers array is untouched. This is because map returns a new array, rather than mutating the original. Mutations are a common source of bugs or unintended behavior in programming, so avoiding mutations unless explicitly necessary is typically a good practice.

Let's write our own map() method like we did with forEach() above.

function myMap(array, callback) {
  const results = [];

  for (let i = 0; i < array.length; i++) {
    const result = callback(array[i]);
    results.push(result);
  }

  return results;
}

const numbers = [1, 2, 3, 4];
const squaredNumbers = myMap(numbers, (number) => number * number);

console.log(squaredNumbers); // [1, 4, 9, 16]

filter()

filter() is a very well named method. It's used to filter down an array to a new array that contains only the elements that pass some condition. From MDN:

The filter() method creates a new array with all elements that pass the test implemented by the provided function.

Let's stick with the list of numbers we used in our map() examples above, and try filtering down to only the even numbers.

const numbers = [1, 2, 3, 4];

const evens = numbers.filter((number = number % 2 === 0));
console.log(evens); // [2, 4]

Every time the callback function returns true, the element will be added to our resulting array. With that implementation in mind, let's create our own filter() as we did with the other array methods:

function myFilter(array, callback) {
  const results = [];

  for (let i = 0; i < array.length; i++) {
    const element = array[i];

    if (callback(element)) {
      results.push(element);
    }
  }

  return results;
}

const numbers = [1, 2, 3, 4];
const odds = myFilter(numbers, (number) => number % 2 !== 0);

console.log(odds); // [1, 3]

reduce()

reduce() is perhaps the most complex of these standard array methods. Here's MDN's description:

The reduce() method executes a reducer function (that you provide) on each element of the array, resulting in a single output value.

The most common example of a use case for "reduce" is calculating the sum of all elements in an array. In this case, we're "reducing" many elements of an array into a single value: the sum. The signature for the "reducer" callback is a function that accepts an "accumulator" (the current value of the resulting single value) and the current element. It's common to see these arguments represented as acc and curr or similar. reduce() also accepts a second argument for the initial value of the accumulator. Let's take a look at our "sum of array elements" use case in code:

const numbers = [2, 4, 6, 8];

const sum = numbers.reduce((acc, curr) => {
  return acc + curr;
}, 0);

// Or, with shorthand
const sum2 = numbers.reduce((sum, num) => sum + num, 0);

console.log(sum); // 20

reduce() is useful for aggregating data about an array. For instance, determining the unique values for some string, and calculating how many times each value appears. Because of its relative complexity, workflows involving reduce() may often be represented more coherently with standard loops or a forEach(). Let's compare/contract an aggregation workflow with a plain loop and reduce() below:

// Let's say we want to derive a new data structure from this array of users.
// We want an object keyed by email, where each value is an array of
// the user's post titles
const users = [
  {
    id: 1,
    email: 'user@yahoo.com',
    posts: [{ title: 'Nope', category: 'News' }],
  },
  {
    id: 2,
    email: 'user@gmail.com',
    posts: [
      { title: 'Good Post', category: 'Fishing and Hunting' },
      { title: 'Another good one', category: 'Topiaries' },
    ],
  },
  {
    id: 3,
    email: 'user2@gmail.com',
    posts: [
      { title: 'Best Post Ever!', category: 'Construction' },
      { title: 'A post', category: 'Starbucks Secret Menu' },
    ],
  },
];

// Plain loop implementation
const result1 = {};
for (const user of users) {
  result[user.email] = user.posts;
}

// Reduce implementation
const result2 = users.reduce((acc, curr) => {
  acc[user.email] = user.posts;

  return acc;
}, {});

reduce() is definitely the array method that's most conducive to overly clever code, but for succinct workflows it can still be an expressive method.

Others

There are a few other Array methods that are commonly used, and are a bit more straightforward than those we've looked at above. It's time for a ⚡lightning round ⚡!

// `.some` - return true as long as at least one element satisfies the condition
// returned by the callback
const hasEvenNumber = [1, 2, 3, 4].some((num) => num % 2 === 0);
console.log(hasEvenNumber); // true

// `.every` - return true as long as ALL elements satisfy the condition returned by the callback
const allEven = [2, 4, 6, 8].every((num) => num % 2 === 0);
console.log(allEven); // true

// `.find` - return the first value that satifsies the condition returned by the callback
const firstEven = [1, 2, 3, 4].find((num) => num % 2 === 0);
console.log(firstEven); // 2

// `.includes` - return true if the value is present in the array
const hasTwo = [1, 2, 3, 4].includes(2);
console.log(hasTwo); // true

Further Reading

If you're interested in flexing your Array knowledge, I provided a quiz when I gave this blog post as a talk at work. You can clone the GitHub repo, and attempt to update that test file and make all the tests pass.