Chapter 12: Arrays and Collections
Arrays and collection types are at the heart of JavaScript programming. They allow you to group related values, organize data, and perform operations across lists, sequences, or sets of items. From storing a few numbers to managing complex datasets, JavaScript’s array and collection features provide the tools needed for efficient data manipulation.
The array is the most common of these structures. It is an ordered, zero-indexed list that can hold any type of value such as numbers, strings, objects, or even other arrays. Although arrays are technically objects, they come with a rich set of methods designed for traversal, transformation, and aggregation.
Beyond traditional arrays, modern JavaScript includes higher-order methods such as map(), filter(), and reduce() that enable expressive functional programming techniques. These methods help process collections declaratively, reducing the need for manual loops and temporary variables.
It is also important to understand which array operations modify the original data (mutating methods) and which return new arrays instead. Knowing this distinction helps prevent subtle bugs and keeps your code predictable when working with shared or reused data.
push(), pop(), and splice() modify the original array. Others like map(), filter(), and slice() return a new one, leaving the source untouched.
In addition to arrays, JavaScript provides specialized collection types such as Map, Set, WeakMap, and WeakSet. These offer efficient ways to store key-value pairs or unique elements and are especially useful for caching, lookup tables, and managing object references safely.
This chapter explores all these structures in turn. It begins with array creation and iteration, moves on to functional transformation methods, examines the difference between mutating and non-mutating operations, introduces typed arrays for binary data, and concludes with a look at the modern collection classes that extend JavaScript’s handling of structured data.
Array creation and iteration
Arrays in JavaScript are ordered lists of values, indexed from zero, that can hold elements of any type. You can create them using array literals, the Array constructor, or from existing iterable objects. Array literals are by far the most common and readable form.
// Literal syntax
let numbers = [1, 2, 3, 4];
// Constructor form
let fruits = new Array('apple', 'banana', 'cherry');
// From another iterable
let chars = Array.from('hello'); // ['h', 'e', 'l', 'l', 'o']
Arrays are dynamic, so they can grow or shrink as elements are added or removed. You can assign values by index or use methods such as push() and pop().
let list = [];
list.push('a');
list.push('b');
console.log(list); // ['a', 'b']
list[2] = 'c';
console.log(list.length); // 3
undefined elements, and some methods skip them during iteration.
JavaScript provides several ways to iterate through arrays, each suited to different purposes. The classic for loop gives fine control over indexes, while the more modern for...of loop iterates directly over values.
let nums = [10, 20, 30];
// Traditional for loop
for (let i = 0; i < nums.length; i++) {
console.log(nums[i]);
}
// for...of loop
for (let value of nums) {
console.log(value);
}
Array iteration methods offer a declarative alternative. forEach() runs a callback function once for every element.
nums.forEach(n => console.log(n * 2));
// Output: 20, 40, 60
for...in loop is designed for object properties, not arrays. It iterates over keys and can include inherited properties, so prefer for...of or forEach() when working with arrays.
Arrays also expose useful factory methods for creation and filling. Array.of() creates an array from given arguments, while fill() and copyWithin() can modify contents efficiently.
let arr = Array.of(1, 2, 3); // [1, 2, 3]
arr.fill(0); // [0, 0, 0]
Finally, array destructuring allows quick unpacking of values into separate variables, improving clarity and conciseness when working with lists of known size.
let coords = [12, 34];
let [x, y] = coords;
console.log(x, y); // 12 34
These techniques form the foundation for all further work with arrays. Once you understand how to create and iterate through them, you can move on to transforming, filtering, and reducing collections with functional methods such as map(), filter(), and reduce().
map, filter, and reduce
Arrays in JavaScript include a set of higher-order methods that make it easy to transform, filter, and aggregate data without manual loops. These methods (map(), filter(), and reduce()) are central to a functional programming style, where operations are expressed in terms of data transformations rather than step-by-step instructions.
The map() method creates a new array by applying a function to every element of the original. It does not modify the source array.
let numbers = [1, 2, 3, 4];
let squares = numbers.map(n => n * n);
console.log(squares); // [1, 4, 9, 16]
map() method is perfect for one-to-one transformations where every input element becomes exactly one output element.
The filter() method returns a new array containing only those elements for which a given function returns true. It is ideal for selecting subsets of data.
let words = ['apple', 'bat', 'banana', 'cat'];
let longWords = words.filter(w => w.length > 3);
console.log(longWords); // ['apple', 'banana']
The reduce() method is more general. It processes all elements and combines them into a single result, carrying forward an accumulated value with each iteration. This is useful for totals, averages, or any type of aggregation.
let prices = [10, 15, 20];
let total = prices.reduce((sum, value) => sum + value, 0);
console.log(total); // 45
reduce() (the second argument). Without it, the first array element becomes the initial accumulator, which can cause subtle bugs or errors when the array is empty.
Two closely related methods, some() and every(), test whether any or all elements satisfy a condition.
let nums = [2, 4, 6];
console.log(nums.every(n => n % 2 === 0)); // true
console.log(nums.some(n => n > 5)); // true
Another useful method, find(), returns the first element that matches a condition, while findIndex() returns its position in the array.
let people = [
{ name: 'Alice', age: 30 },
{ name: 'Bob', age: 40 }
];
let result = people.find(p => p.age > 35);
console.log(result.name); // Bob
For flattening nested arrays, use flat() and flatMap(). The latter combines mapping and flattening into one concise step.
let nested = [1, [2, 3], [4, [5]]];
console.log(nested.flat(2)); // [1, 2, 3, 4, 5]
let doubled = [1, 2, 3].flatMap(n => [n, n * 2]);
console.log(doubled); // [1, 2, 2, 4, 3, 6]
let result = [1, 2, 3, 4, 5]
.filter(n => n % 2 === 0)
.map(n => n * n)
.reduce((a, b) => a + b, 0);
console.log(result); // 20
Together, map(), filter(), and reduce() form the core of functional-style array processing in JavaScript. When combined with other helpers like some(), every(), find(), and flatMap(), they provide a concise, expressive, and immutable approach to working with data collections.
Mutating vs non-mutating methods
Some array methods in JavaScript change the array they are called on, while others leave the original untouched and instead return a new array. Understanding which methods mutate and which do not is essential for writing predictable and bug-free code, especially when data is shared across different parts of a program.
Mutating methods directly modify the array’s contents. They may add, remove, or rearrange elements in place, altering both the data and its length. Examples include:
push()adds elements to the endpop()removes the last elementshift()removes the first elementunshift()adds elements to the startsplice()adds or removes elements at specific positionssort()sorts the array in placereverse()reverses element orderfill()replaces all values with a given onecopyWithin()overwrites part of the array with its own elements
let fruits = ['apple', 'banana', 'cherry'];
fruits.push('date');
console.log(fruits); // ['apple', 'banana', 'cherry', 'date']
fruits.reverse();
console.log(fruits); // ['date', 'cherry', 'banana', 'apple']
Non-mutating methods return new arrays or values without touching the original data. These methods are safer in functional-style programming and make it easier to reason about program state.
concat()merges arrays into a new oneslice()extracts a section into a new arraymap()transforms elementsfilter()selects elements based on a conditionreduce()combines values into one resultflat()flattens nested arraystoSorted()returns a sorted copy (ES2023+)toReversed()returns a reversed copy (ES2023+)
let nums = [3, 1, 2];
let sorted = nums.toSorted();
console.log(sorted); // [1, 2, 3]
console.log(nums); // [3, 1, 2] — original unchanged
toSorted() and toReversed(). These preserve immutability while offering familiar functionality.
When working on large projects or functional-style codebases, preferring non-mutating methods helps maintain data integrity and avoids unintentional changes. However, mutating methods can still be useful for small, localized operations where performance or simplicity matters.
The key is to know which approach you are using at any given time. Mutating methods alter the existing array in place, while non-mutating methods return a new array, leaving the original untouched. Choosing between them is as much about code style and predictability as it is about performance.
Typed arrays
Typed arrays provide a way to handle raw binary data in JavaScript. Unlike regular arrays, which can hold any type of value and have variable element sizes, typed arrays are fixed-length and store elements of a single numeric type. They are designed for efficient interaction with low-level data, such as graphics buffers, audio streams, and files.
A typed array is not a standalone structure but a view into an underlying ArrayBuffer, which represents a fixed-size block of memory. Each typed array interprets that buffer as a sequence of a specific numeric format, such as 8-bit integers or 32-bit floats.
// Create a buffer of 8 bytes
let buffer = new ArrayBuffer(8);
// Create a 32-bit integer view on the buffer
let view = new Int32Array(buffer);
view[0] = 12345;
view[1] = 67890;
console.log(view); // Int32Array(2) [12345, 67890]
Each typed array type corresponds to a specific data representation. Common variants include:
Int8Array8-bit signed integersUint8Array8-bit unsigned integersInt16Array16-bit signed integersUint16Array16-bit unsigned integersInt32Array32-bit signed integersUint32Array32-bit unsigned integersFloat32Array32-bit floating pointFloat64Array64-bit floating point
You can also create a typed array directly from a standard array or iterable. The values are automatically converted to the target numeric type.
let floats = new Float32Array([1.5, 2.5, 3.5]);
console.log(floats[1]); // 2.5
Typed arrays are especially useful in contexts such as WebGL, audio processing, or working with binary file formats, where precise control over memory layout and performance is essential.
You can also create multiple views over the same buffer, allowing the same memory region to be interpreted in different ways:
let buffer2 = new ArrayBuffer(4);
let bytes = new Uint8Array(buffer2);
let ints = new Int32Array(buffer2);
bytes[0] = 0x78;
bytes[1] = 0x56;
bytes[2] = 0x34;
bytes[3] = 0x12;
console.log(ints[0].toString(16)); // 12345678 (depends on endianness)
While most JavaScript code works comfortably with regular arrays, typed arrays bridge the gap between JavaScript and systems-level performance. They allow efficient number crunching and binary manipulation while still using familiar array-like syntax.
Map, Set, WeakMap, and WeakSet
Beyond arrays, JavaScript provides several collection types designed for more specialized data storage: Map, Set, WeakMap, and WeakSet. These structures offer powerful alternatives to plain objects and arrays, each with its own unique characteristics and use cases.
Map
A Map is a collection of key–value pairs, similar to an object, but with significant differences. Unlike objects, map keys can be of any type, including objects, functions, or even NaN. Maps also maintain the order in which entries were inserted.
let capitals = new Map();
capitals.set('France', 'Paris');
capitals.set('Italy', 'Rome');
capitals.set('Japan', 'Tokyo');
console.log(capitals.get('Italy')); // Rome
console.log(capitals.size); // 3
Maps can be created directly from arrays of pairs, and they provide built-in iteration methods such as forEach() and for...of.
let nums = new Map([[1, 'one'], [2, 'two'], [3, 'three']]);
for (let [key, value] of nums) {
console.log(key, value);
}
Map when keys are not guaranteed to be strings, or when you need predictable insertion order and efficient lookups.
Set
A Set is an unordered collection of unique values. Duplicate entries are ignored automatically. Sets are ideal for membership testing or deduplicating arrays.
let colors = new Set(['red', 'green', 'blue', 'red']);
console.log(colors.size); // 3
colors.add('yellow');
console.log(colors.has('blue')); // true
You can also convert sets to arrays using the spread operator or Array.from().
let arr = [...colors];
console.log(arr); // ['red', 'green', 'blue', 'yellow']
Set object stores values by reference, so two separate but identical objects are considered distinct entries.
WeakMap
WeakMap is similar to Map, but it only accepts objects as keys, and those keys are held “weakly.” This means that if the key object becomes unreachable elsewhere in your program, it can be garbage-collected automatically, freeing memory. WeakMap is not iterable and does not expose its size for security and performance reasons.
let wm = new WeakMap();
let obj = {};
wm.set(obj, 'secret data');
console.log(wm.get(obj)); // secret data
obj = null; // key and value may now be garbage-collected
WeakMap is often used to store metadata or private data associated with objects without preventing their cleanup.
WeakSet
WeakSet is the set equivalent of WeakMap. It stores only objects, and its entries are also held weakly, allowing garbage collection when objects are no longer referenced elsewhere. Like WeakMap, it is not iterable and has no size property.
let ws = new WeakSet();
let user = { name: 'Alice' };
ws.add(user);
console.log(ws.has(user)); // true
user = null; // object may now be collected
WeakMap and WeakSet are useful for managing memory safely when storing associations or flags for objects that have independent lifetimes.
Together, Map, Set, WeakMap, and WeakSet give JavaScript developers a versatile collection toolkit. Use Map for key–value data, Set for unique lists, and their weak counterparts for temporary or memory-sensitive object tracking.