JavaScript Arrays
A JavaScript array is an ordered, zero-indexed collection of values that can grow or shrink at runtime and hold any mix of data types — numbers, strings, objects, functions, even other arrays — in the same list. Arrays are one of the most heavily used tools in the language because nearly every real program needs to work with a list of things: users, prices, DOM elements, API results. Understanding exactly how arrays store data, resize themselves, and expose dozens of built-in methods is essential to writing clean, correct JavaScript.
Overview / How Arrays Work
Conceptually, an array is a numbered list. The first element sits at index 0, the second at index 1, and so on. This is called zero-based indexing, and it trips up almost every beginner at some point — an array with 5 elements has valid indexes 0 through 4, not 1 through 5.
Under the hood, a JavaScript array is actually a special kind of object. Its indexes are really string keys (\"0\", \"1\", \"2\"…) attached to the object, and it carries one extra piece of magic: a length property that automatically updates whenever you add or remove elements. You can prove this to yourself:
const arr = [10, 20, 30];
console.log(typeof arr);
console.log(Array.isArray(arr));
console.log(Object.keys(arr));
Output:
object
true
[ '0', '1', '2' ]
typeof reports \"object\" because arrays inherit from Object, which is why you must use Array.isArray() (never typeof) to reliably detect an array. Internally, JavaScript engines like V8 optimize arrays heavily when they are "packed" (no gaps, consistent element types) and fall back to slower, more general-object-like storage when an array becomes "holey" (has gaps) or mixes wildly different types. This is one reason keeping arrays dense and type-consistent is good for performance, not just readability.
Because the length property is tracked automatically, assigning to an out-of-range index silently grows the array (and can create gaps), while setting length directly can truncate it. Arrays are also reference types: variables don’t hold the array itself, they hold a pointer to it in memory, which matters enormously when copying or passing arrays around (see Common Mistakes below).
Syntax
There are several ways to create an array. The literal form is preferred almost everywhere in modern code:
// Array literal (preferred)
const arr1 = [1, 2, 3];
// Array constructor
const arr2 = new Array(1, 2, 3);
// Empty array with preset length (rarely useful directly)
const arr3 = new Array(5);
// Array.of and Array.from
const arr4 = Array.of(7);
const arr5 = Array.from(\"abc\");
console.log(arr1, arr2, arr3, arr4, arr5);
Output:
[ 1, 2, 3 ] [ 1, 2, 3 ] [ <5 empty items> ] [ 7 ] [ 'a', 'b', 'c' ]
[1, 2, 3]— the array literal, the idiomatic way to write arrays.new Array(1, 2, 3)— equivalent to the literal, but rarely used; confusingly,new Array(5)with a single number creates an empty array of length 5 instead of an array containing the number 5.Array.of(7)— always creates a one-element array[7], sidestepping the single-number ambiguity of the constructor.Array.from(iterable)— builds an array from any iterable (strings,Set,Map,NodeList, etc.) or array-like object.
Examples
Example 1: Creating, reading, and mutating
const fruits = [\"apple\", \"banana\", \"cherry\"];
console.log(fruits);
console.log(fruits[0]);
console.log(fruits.length);
fruits[3] = \"date\";
console.log(fruits);
Output:
[ 'apple', 'banana', 'cherry' ]
apple
3
[ 'apple', 'banana', 'cherry', 'date' ]
Reading an element uses square-bracket notation with its index. Writing to an index that already exists overwrites it; writing to fruits[3] when the array only had indexes 0–2 grows the array and bumps length to 4 automatically.
Example 2: Chaining common array methods
const numbers = [5, 12, 8, 130, 44];
numbers.push(21);
console.log(numbers);
const doubled = numbers.map(n => n * 2);
console.log(doubled);
const bigNumbers = numbers.filter(n => n > 10);
console.log(bigNumbers);
const total = numbers.reduce((sum, n) => sum + n, 0);
console.log(total);
Output:
[ 5, 12, 8, 130, 44, 21 ]
[ 10, 24, 16, 260, 88, 42 ]
[ 12, 130, 44, 21 ]
220
push mutates numbers in place and returns the new length (which we ignore here). map and filter both return brand-new arrays without touching the original, and reduce folds every element down into a single value — here, a running sum starting from 0.
Example 3: Arrays of objects, sorting, and destructuring
const students = [
{ name: \"Ava\", score: 88 },
{ name: \"Liam\", score: 95 },
{ name: \"Noah\", score: 72 }
];
const sorted = [...students].sort((a, b) => b.score - a.score);
console.log(sorted.map(s => s.name));
const [top, ...rest] = sorted;
console.log(`Top student: ${top.name} (${top.score})`);
console.log(`Remaining: ${rest.length}`);
const passed = students.every(s => s.score >= 70);
console.log(`All passed: ${passed}`);
Output:
[ 'Liam', 'Ava', 'Noah' ]
Top student: Liam (95)
Remaining: 2
All passed: true
This example combines several idioms at once: the spread operator [...students] makes a shallow copy so sort (which mutates!) doesn’t disturb the original array; array destructuring with a rest element (const [top, ...rest]) pulls the first item and bundles the remainder; and every checks a condition across all elements, short-circuiting to false the moment one fails.
Under the Hood: Mutating vs. Non-Mutating Methods
One of the biggest sources of array bugs is not knowing whether a method changes the original array or returns a new one. Memorize this split:
| Method | Mutates original? | What it does |
|---|---|---|
push(x) / pop() |
Yes | Add/remove at the end |
unshift(x) / shift() |
Yes | Add/remove at the start |
splice() |
Yes | Insert/remove/replace at any position |
sort() / reverse() |
Yes | Reorder elements in place |
map() / filter() |
No | Return a new transformed/subset array |
slice() / concat() |
No | Return a new array (copy or merge) |
reduce() / forEach() |
No* | Fold to a value / run a side effect per item |
find() / includes() / indexOf() |
No | Search for an element or its index |
*forEach and reduce don’t mutate the array themselves, but the callback you pass them certainly can if you write to the original array inside it.
When JavaScript calls numbers.push(21), the engine appends the value at index numbers.length, then increments length by one — it does not create a new array. By contrast, numbers.map(fn) allocates a brand-new array, walks every index of the original from 0 to length - 1, calls fn on each element, and pushes the result into the new array, leaving the original completely untouched.
Common Mistakes
1. Assuming = copies an array
Because arrays are reference types, assigning one variable to another just copies the pointer, not the data:
const original = [1, 2, 3];
const copy = original;
copy.push(4);
console.log(original);
Output:
[ 1, 2, 3, 4 ]
Even though only copy was modified, original changed too — they’re the same array in memory. To make an independent copy, use the spread operator or slice():
const original = [1, 2, 3];
const copy = [...original];
copy.push(4);
console.log(original);
console.log(copy);
Output:
[ 1, 2, 3 ]
[ 1, 2, 3, 4 ]
Note that [...original] is only a shallow copy: if the elements are themselves objects or arrays, both copies still share references to those nested values.
2. Using delete to remove an element
const arr = [\"a\", \"b\", \"c\"];
delete arr[1];
console.log(arr);
console.log(arr.length);
Output:
[ 'a', <1 empty item>, 'c' ]
3
delete removes the value but leaves a hole (an "empty item") at that index, and length stays at 3 — almost never what you want. Use splice() instead, which actually shifts the remaining elements down and updates length:
const arr = [\"a\", \"b\", \"c\"];
arr.splice(1, 1);
console.log(arr);
console.log(arr.length);
Output:
[ 'a', 'c' ]
2
Best Practices
- Prefer array literals (
[]) overnew Array()— they’re shorter and avoid the single-number ambiguity of the constructor. - Use
constfor arrays whenever possible; you can stillpush/splice/mutate the contents even though the variable binding is constant. - Reach for
map,filter, andreduceinstead of manualforloops when transforming data — they’re more declarative and avoid off-by-one index bugs. - Copy arrays with the spread operator (
[...arr]) orstructuredClone(arr)for deep copies, rather than assigning directly. - Never use
for...into iterate arrays — it iterates keys (including inherited or non-index ones) in no guaranteed numeric order. Usefor...of,forEach, or a classic indexedforloop instead. - Check for arrays with
Array.isArray(value), nevertypeof value === \"array\"(which doesn’t exist) ortypeof value === \"object\"(too broad). - Remember that
sort()converts elements to strings by default, so[10, 2, 1].sort()gives[1, 10, 2]. Always pass a comparator for numbers:arr.sort((a, b) => a - b).
Practice Exercises
- Exercise 1: Given
const prices = [19.99, 5.5, 42, 3.75, 8], write code that returns only the prices above10, then sums the remaining ones. What single number does your code print? - Exercise 2: Write a function
removeDuplicates(arr)that takes an array of numbers and returns a new array with duplicate values removed, without mutating the input. (Hint: aSetcombined with the spread operator can do this in one line.) - Exercise 3: Given an array of objects
[{ id: 1, done: false }, { id: 2, done: true }, { id: 3, done: false }], use array methods to produce an array containing only theidvalues of the tasks wheredoneisfalse.
Summary
- Arrays are ordered, zero-indexed, resizable lists that can hold mixed data types, and are internally a special kind of object.
lengthupdates automatically as you add or remove elements; useArray.isArray(), nottypeof, to detect one.- Some methods mutate the original array (
push,pop,splice,sort,reverse); others return a new array and leave the original untouched (map,filter,slice,concat). - Assigning an array to another variable copies the reference, not the data — use the spread operator or
slice()for an independent copy. - Avoid
deleteon array elements; it leaves holes instead of shrinking the array. Usesplice()to truly remove an element. - Destructuring (
const [a, ...rest] = arr) and the spread operator ([...arr]) are the idiomatic modern ways to unpack and copy arrays.
