Arrays are everywhere in JavaScript. Once you’re comfortable with the basics, it’s worth learning some best practices and gotchas that show up in real codebases.
Prefer Non‑Mutating Methods for Shared State
Many array methods mutate the original array:
push,popshift,unshiftsplice,sort,reverse
In state‑heavy code (React, Redux, or anywhere multiple parts of your app share the same data), mutating arrays in place can cause subtle bugs.
Prefer non‑mutating patterns when working with shared state:
// Bad in shared state: mutates the original arraystate.items.push(newItem);
// Better: create a new arrayconst newState = { ...state, items: [...state.items, newItem],};Similarly, instead of splice to remove an item:
// Remove item by id without mutationconst filtered = state.items.filter((item) => item.id !== idToRemove);Mutation is fine for small, local variables—but be extra cautious around shared or long‑lived arrays.
Don’t Use for...in on Arrays
for...in iterates over keys (and includes inherited properties), making it a poor fit for arrays.
Array.prototype.extra = 123; // bad practice, but it happens
const arr = [10, 20, 30];
for (const key in arr) { console.log(key);}// '0'// '1'// '2'// 'extra' (unexpected)Use for...of or array methods instead:
for (const value of arr) { console.log(value);}// 10// 20// 30Beware Sparse Arrays and Array(length)
Using new Array(length) creates an array with empty slots, not actual undefined values. Some methods behave unexpectedly with these “holes”.
const a = new Array(3);
console.log(a.length); // 3console.log(a[0]); // undefined, but the slot is "empty"
// map skips empty slotsconst doubled = a.map(() => 2);console.log(doubled); // [ <3 empty items> ]Prefer explicit filling:
// Create an array of 3 zerosconst zeros = Array(3).fill(0);console.log(zeros); // [0, 0, 0]If you see arrays with <empty> items in logs, you’re likely dealing with a sparse array.
Using length Intentionally
The length property is writable. Setting it lower truncates the array.
const values = [1, 2, 3, 4, 5];
values.length = 3;console.log(values); // [1, 2, 3]This can be a deliberate way to clear or truncate arrays:
// Clear an array in placevalues.length = 0;console.log(values); // []But accidentally assigning to length can lose data, so avoid writing to it unless you really mean to.
Sorting Pitfalls
Array.prototype.sort:
- Mutates the array in place.
- Converts elements to strings by default and sorts lexicographically, which can be surprising for numbers.
const numbers = [2, 10, 1];
numbers.sort();console.log(numbers); // [1, 10, 2] (string comparison)Always pass a compare function for numbers and objects:
const sorted = [...numbers].sort((a, b) => a - b);console.log(sorted); // [1, 2, 10]console.log(numbers); // [2, 10, 1] (unchanged because we copied first)Copying with [...] (or slice()) before sorting is a good habit when you want to avoid mutating the original array.
Equality: Arrays Compared by Reference, Not Value
Two arrays with the same contents are not equal unless they’re the same reference.
const a = [1, 2, 3];const b = [1, 2, 3];
console.log(a === b); // false
const c = a;console.log(a === c); // trueTo compare arrays by value, you’ll need to:
- Check length and each element yourself, or
- Use a helper function / testing library that supports deep equality.
Returning New Arrays from Functions
When writing helpers that work with arrays, it’s usually safer to return new arrays instead of mutating the input. This makes functions easier to reason about and reuse.
// Mutating versionfunction addTagInPlace(tags, tag) { tags.push(tag);}
// Immutable versionfunction withTag(tags, tag) { return [...tags, tag];}
const original = ['js'];const updated = withTag(original, 'arrays');
console.log(original); // ['js']console.log(updated); // ['js', 'arrays']Handling Empty Arrays Correctly
Many array methods behave differently on empty arrays, so it’s good to know the details:
mapandfilteron an empty array simply return a new empty array.reducethrows an error if you don’t provide an initial value.
const empty = [];
// Safe: returns []const doubled = empty.map((x) => x * 2);
// Unsafe: TypeError: reduce of empty array with no initial value// const total = empty.reduce((sum, x) => sum + x);
// Safe: always provide an initial value when array might be emptyconst totalSafe = empty.reduce((sum, x) => sum + x, 0);console.log(totalSafe); // 0Whenever an array might be empty, prefer supplying an initial value to reduce.
Copying and Cloning Arrays
Shallow copies are easy with:
- Spread:
[...array] slice():array.slice()Array.from(arrayLike)
const original = [1, 2, 3];
const copy1 = [...original];const copy2 = original.slice();
copy1.push(4);
console.log(original); // [1, 2, 3]console.log(copy1); // [1, 2, 3, 4]Remember: these are shallow copies. If the array contains objects, the objects are shared.
const users = [{ name: 'Ada' }, { name: 'Grace' }];const copy = [...users];
copy[0].name = 'Changed';
console.log(users[0].name); // 'Changed' (same object reference)For deep cloning nested arrays/objects, consider structuredClone or a dedicated library.
Summary
- Prefer non‑mutating patterns (
map,filter,slice, spreading) for shared or long‑lived state. - Avoid
for...inon arrays; usefor...ofor array methods. - Watch out for sparse arrays and unintended
lengthchanges. - Always provide a comparator to
sortwhen working with numbers or objects. - Treat arrays as reference values and be explicit about when you’re mutating vs returning new arrays.
With these practices, your array‑heavy code will be more predictable, easier to test, and friendlier to work with in larger applications.