Arrays Are Not Always the Answer: Choosing the Right Data Structure in JavaScript
Arrays are often the first data structure we learn in JavaScript. They’re simple ([]), syntactically convenient, and packed with built-in methods like .push(), .pop(), .filter(), .map(), and .reduce(). Early on, we reach for arrays for nearly every problem because they’re familiar, easy to create, and seem to solve most basic tasks.
However, using arrays as a catch-all can result in:
Performance pitfalls when operations like membership checks, middle deletions, or front removals become O(n).
Semantic confusion—is this array a Set, a Stack, a Queue, or just a bag of items?
Scalability headaches, since code that works on small arrays may buckle under large data volumes.
Below, we’ll explore key trade-offs, modern ES6 alternatives, and concise code snippets so you know when to reach for Sets, Maps, linked lists, and more.
Why Arrays Become a Crutch
Performance Pitfalls
Membership checks using
includes()run in O(n) each time.Queue simulations with
shift()suffer O(n) re-indexing on removal.Middle insertions/deletions via
splice()also take O(n).
Semantic Confusion
When you see:const data = []; data.push(item); data.includes(item); data.splice(index, 1);...is that a Set, a Stack, a Queue, or just an untyped list? Intent gets lost.
Scalability Issues
What works fast for 10 or 100 items can slow dramatically at 10,000+. Refactoring array-heavy logic is error-prone.
Modern ES6 Alternatives
Sets for Fast Uniqueness & Membership
A Set stores unique values with average O(1) for .add(), .has(), and .delete().
const arr = [1, 2, 2, 3, 3, 3, 4];
const unique = [...new Set(arr)]; // [1,2,3,4]
const whitelist = new Set(["alice", "bob", "charlie"]);
function isAllowed(name) {
return whitelist.has(name); // O(1) lookup
}
When to use: you only care about membership or uniqueness. Great for filtering duplicates and whitelists.
Maps for Reliable Key/Value Storage
A Map functions like an object but:
Accepts any key type (primitives or objects).
Preserves insertion order.
Exposes a
.sizeproperty.
const text = "hello world hello hello world";
const counts = new Map();
text.split(" ").forEach(word => {
counts.set(word, (counts.get(word) || 0) + 1);
});
for (const [word, freq] of counts) {
console.log(word, freq);
}
// hello 3
// world 2
When to use: you need a true key/value store with complex keys, ordered iteration, or constant-time size checks.
Choosing Objects vs. Maps
Plain Objects
{}are best for simple string-keyed data and JSON serialization.Maps shine when you need non-string keys, guaranteed iteration order, and efficient
.sizelookups.
Linked Lists: O(1) Middle Operations
Arrays incur O(n) for middle insertions and deletions. With a linked list, you pay O(1) for these operations if you hold a node reference—at the cost of extra pointer storage and no direct random access.
Singly Linked List (O(1) append):
class ListNode {
constructor(val) { this.val = val; this.next = null; }
}
class SinglyLinkedList {
constructor() { this.head = this.tail = null; }
append(val) {
const node = new ListNode(val);
if (!this.head) this.head = node;
else this.tail.next = node;
this.tail = node;
}
}
Use case: stacks, queues, or adjacency lists where you frequently add or remove nodes at known positions.
Stacks & Queues: Clarity Over Convenience
Stacks (LIFO)
Using an array’s .push() and .pop() is fine for small-scale stacks:
const stack = [];
stack.push(1);
stack.push(2);
console.log(stack.pop()); // 2
But if you want clear intent or a pluggable backend:
class Stack {
constructor() { this.data = []; }
push(x) { this.data.push(x); }
pop() { return this.data.pop(); }
peek() { return this.data[this.data.length - 1]; }
}
Queues (FIFO)
Avoid shift() on arrays for large queues—.shift() is O(n). Instead, back with a linked list:
class Queue {
constructor() { this.head = this.tail = null; }
enqueue(val) {
const node = { value: val, next: null };
if (!this.head) this.head = node;
else this.tail.next = node;
this.tail = node;
}
dequeue() {
if (!this.head) return null;
const val = this.head.value;
this.head = this.head.next;
if (!this.head) this.tail = null;
return val;
}
}
When to use: heavy enqueue/dequeue workloads where O(1) operations matter.
Conclusion
Arrays excel at random access (arr[i]) and end-operations (push, pop). Beyond that, consider alternatives:
Set for uniqueness and fast membership.
Map for flexible key/value storage.
Linked List for constant-time middle inserts/deletes.
Stack/Queue classes for clear intent and scalable behavior.
By understanding these trade-offs, you’ll write faster code, express your intent more clearly, and build systems that scale without painful refactors. Next time you reach for const data = [], pause and ask: Is there a better data structure for this job?
Happy coding!