Skip to content
Permalink
Branch: master
Find file Copy path
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
349 lines (249 sloc) 13.1 KB

JavaScript arrays are an essential part of the language. Fundamentally, an array is a value that stores an ordered list of other values. But JavaScript arrays come with a lot of nuances and surprises. In this article, I'll provide an overview of what you need to know about JavaScript arrays.

Creating Arrays

The most common way to create an array in JavaScript is using square brackets []:

// `arr` is an array with 3 elements
const arr = ['a', 'b', 'c'];

arr.length; // 3
arr[0]; // 'a'
arr[1]; // 'b'
arr[2]; // 'c'

JavaScript also has a built-in Array class. You may see the above array declared this way:

const arr = new Array('a', 'b', 'c');

arr.length; // 3
arr[0]; // 'a'
arr[1]; // 'b'
arr[2]; // 'c'

However, you should avoid using new Array() unless you have a very good reason to. That's because, if new Array(v) receives only one parameter and v is an integer between 0 and 2^31, JavaScript will interpret that to mean you want to create an empty array of length v.

const arr = new Array(5);

arr.length; // 5
arr[0]; // undefined

ES6 introduced an Array.of() function that avoids this gotcha:

const arr = Array.of(5);

arr.length; // 1
arr[0]; // 5

Copying Arrays

There's 4 common ways to copy an array. First, given an array arr, you can use the Array#concat() function to concat the values in arr onto an empty array:

const arr = ['a', 'b', 'c'];

const arr2 = [].concat(arr);
arr2.length; // 3
arr2 === arr; // false

Second is the Array#slice() function. The slice() function also takes 2 parameters, a begin and an end.

const arr = ['a', 'b', 'c'];

const arr2 = arr.slice();
arr2.length; // 3
arr2 === arr; // false

// Copy everything from index 1 onwards
arr.slice(1); // ['b', 'c']
// Copy everything from index 0 to index 2, excluding index 2.
arr.slice(0, 2); // ['a', 'b']

slice() and concat() are the most common because they have wide browser support. concat() and slice() are in every modern browser and IE back to IE 5.5, so you're unlikely to find a JavaScript environment that doesn't have these functions. On the other hand, the next two array copying paradigms, Array.from() and spread syntax, are not supported in any version of Internet Explorer.

ES6 introduced an Array.from() function. What makes Array.from() special is that it can convert an arbitrary JavaScript iterable into an array, including maps and sets. Arrays are iterables, so you can use Array.from() to copy an array.

const arr = ['a', 'b', 'c'];

const arr2 = Array.from(arr);
arr2.length; // 3
arr2 === arr; // false

Finally, ES6 also introduced spread syntax for copying arrays. Spread means ...arr is syntactically equivalent to 'a', 'b', 'c', including for function calls.

const arr = ['a', 'b', 'c'];

// The '...' is spread syntax.
const arr2 = [...arr];
arr2.length; // 3
arr2 === arr; // false

Checking if a Value is an Array

Do not use JavaScript's typeof operator to check whether a value is an array. That's because arrays are technically objects:

typeof []; // 'object'

The most common way to check whether a value is an array is the Array.isArray() function, which is supported in all modern browsers and IE since IE9.

Array.isArray([1, 2, 3]); // true

Array.isArray(null); // false
Array.isArray('test'); // false
Array.isArray({ 0: 'test' }); // false

You can also use the instanceof operator to check if a value is instanceof Array.

[1, 2, 3] instanceof Array; // true

null instanceof Array; // false
'test' instanceof Array; // false
({ 0: 'test' }) instanceof Array; // false

Iterating and Manipulating Arrays

There are numerous ways to iterate through an array in JavaScript. They all have their tradeoffs, but the 3 most common ways to iterate over arr are:

  • Conventional for loop
for (let i = 0; i < arr.length; ++i) {
  arr[i]; // 'a', 'b', 'c'
}
arr.forEach((val, i) => {
  val; // 'a', 'b', 'c'
  i; // 0, 1, 2
});
for (const val of arr) {
  val; // 'a', 'b', 'c'
}

The conventional for loop and for/of loop both support the break statement to terminate the loop early and the continue statement to skip the rest of the current iteration of the loop. There's no equivalent to break for forEach(), but you can use return as an equivalent to continue for forEach().

To add an element to the end of the array, you can use the Array#push() function.

const arr = ['a', 'b'];

arr.push('c');
arr; // ['a', 'b', 'c']

To add to the beginning of the array, you can use Array#unshift().

const arr = ['b', 'c'];

arr.unshift('a');
arr; ['a', 'b', 'c'];

To insert an element in-place into an array and shift the rest of the array over, you can use Array#splice() (not to be confused with slice()).

const arr = ['a', 'b', 'd'];

// `2` means insert at index 2, `0` means delete 0 elements at index 2
arr.splice(2, 0, 'c');

// `splice()` modified the array in-place
arr; // ['a', 'b', 'c', 'd']

To remove the first or last element in the array, you can use shift() and pop(), respectively. You can also use splice().

const arr = ['a', 'b', 'c'];

arr.shift();
arr; // ['b', 'c']

arr.pop();
arr; // ['b']

Functional Programming

JavaScript arrays also have several functional programming primitives:

These functions are great for chaining function calls, so you can transform arrays without explicit looping. In some cases, chaining syntax can be much cleaner than the equivalent loop.

For example, suppose you have an array of movies:

const movies = [
  { title: 'The Terminator', releaseDate: new Date('1984-10-26') },
  { title: 'Conan the Destroyer', releaseDate: new Date('1984-06-29') },
  { title: 'Predator', releaseDate: new Date('1987-06-12') },
  { title: 'Jingle All the Way', releaseDate: new Date('1996-11-22') }
];

Suppose you want to filter for movies released between 1980 and 1989, and group them by year. You can do this

const byYear = {};
for (const movie of movies) {
  const year = movie.releaseDate.getYear();
  if (year < 1980 || year > 1989) {
    continue;
  }
  byYear[year] = byYear[year] || [];
  byYear[year].push(movie.title);
}

Using filter(), map(), and reduce(), you can do this instead:

const byYear = movies.
  map(m => Object.assign(m, { year: m.releaseDate.getYear() })).
  filter(m => m.year >= 1980 && m.year <= 1989).
  reduce((byYear, m) => {
    byYear[year] = byYear[year] || [];
    byYear[year].push(movie.title);
    return byYear;
  }, {});

Holes

One annoying quirk of JavaScript arrays is holes. For example, the below array has a "hole" at index 1.

const arr = ['a',, 'c'];

arr.length; // 3
arr[1]; // 'undefined'

Holes can also occur if you delete an array index:

const arr = ['a', 'b', 'c'];

delete arr[1];

arr.length; // 3
arr[1]; // 'undefined'

A hole is different from just setting arr[1] = undefined because some methods of iterating an array skip holes. Specifically, forEach() skips holes, but conventional for loops and for/of loops do not.

const arr = ['a',, 'c'];

arr.forEach(v => {
  v; // 'a', 'c'
});

for (let i = 0; i < arr.length; ++i) {
  arr[i]; // 'a', undefined, 'c'
}

for (const val of arr) {
  val; // 'a', undefined, 'c'
}

You're unlikely to see array holes in practice, because it is impossible to represent an array hole in JSON. For example, if you JSON.stringify() an array with holes, JavaScript will replace the hole with null.

const arr = ['a',, 'c'];

JSON.stringify(arr); // '["a",null,"c"]'

Subclassing the Array Class

ES6 introduced the ability to subclass Array. For example, you can declare a custom array class that extends Array like any other ES6 class.

For example, suppose you want a custom array class that supports filtering by property name and value. For example:

class MyArray extends Array {
  filterByProperty(name, value) {
    return this.filter(obj => obj.name === 'value');
  }
}

const blogPosts = MyArray.of([
  { title: 'Introduction to `fs`', tag: 'Node.js' },
  { title: 'Introduction to Arrays`', tag: 'JavaScript' },
  { title: 'Introduction to Maps`', tag: 'JavaScript' }
]);

const filtered = blogPosts.filterByProperty('tag', 'JavaScript');

filtered.length; // 2
filtered instanceof MyArray; // true

Be careful about modifying the array constructor! When you call filter(), map(), etc. on a subclassed array, JavaScript will call the array constructor with a single parameter 0:

class MyArray extends Array {
  constructor(v) {
    console.log('Called Constructor', v);
    super(v);
  }
}

const arr = MyArray.of([1, 2, 3]);

// Prints "Called Constructor 0"
arr.filter(v => v > 2);

If you choose to overwrite the array constructor with additional required parameters, make sure you can handle this case. You can also overwrite filter(), map(), and other functional paradigms to call your custom array constructor correctly.

Moving On

Arrays are everywhere in JavaScript. Unfortunately, the Array API has a broad surface area and there's myriad tradeoffs between ways to achieve the same outcome. Depending on your use case, you may want to use functional programming versus loops, concat() versus spread, etc. Getting the details right is tricky, which is why some people resort to draconian ESLint rules to reduce the likelihood of mistakes. ESLint can help, but it is no substitute for a deep understanding of array fundamentals.

You can’t perform that action at this time.