Data Structures in JavaScript: Complete Guide
Master JavaScript data structures from arrays and objects to custom implementations like stacks, queues, trees, and graphs. Learn when to use each structure and optimize performance.
Table of Contents
- Introduction
- Built-in JavaScript Data Structures
- Custom Data Structures
- Choosing the Right Data Structure
- Performance Analysis
- Real-World Applications
- Best Practices
- Conclusion
Introduction
Data structures are fundamental building blocks of programming that determine how data is organized, stored, and accessed in your applications. In JavaScript, understanding data structures is crucial for writing efficient, scalable code that performs well under various conditions.
JavaScript provides several built-in data structures like arrays and objects, but many problems require custom implementations of classic structures like stacks, queues, linked lists, trees, and graphs. Each data structure has unique characteristics that make it suitable for specific use cases, and choosing the right one can dramatically impact your application’s performance and maintainability.
Whether you’re building a web application, optimizing algorithms, or preparing for technical interviews, a solid grasp of data structures will help you write better code. This guide covers both JavaScript’s native data structures and how to implement custom ones, complete with performance considerations and real-world examples.
By the end of this guide, you’ll understand when to use each data structure, how to implement them efficiently, and how to analyze their performance characteristics. You’ll also learn practical patterns that you can apply immediately in your projects.
Built-in JavaScript Data Structures
JavaScript provides several built-in data structures that are optimized and ready to use. Understanding their characteristics, strengths, and limitations is essential for effective JavaScript development.
Arrays 📋
Arrays are ordered collections of elements, indexed by integers starting from zero. They’re one of the most versatile data structures in JavaScript.
// Creating arraysconst fruits = ["apple", "banana", "orange"];const numbers = new Array(1, 2, 3);const empty = [];
// Array operationsfruits.push("grape"); // Add to end: O(1)fruits.pop(); // Remove from end: O(1)fruits.unshift("mango"); // Add to beginning: O(n)fruits.shift(); // Remove from beginning: O(n)fruits.indexOf("banana"); // Find index: O(n)fruits.includes("apple"); // Check existence: O(n)
// Array methodsconst doubled = numbers.map((x) => x * 2); // Transform: O(n)const evens = numbers.filter((x) => x % 2 === 0); // Filter: O(n)const sum = numbers.reduce((acc, x) => acc + x, 0); // Reduce: O(n)Key Characteristics:
- Dynamic sizing: Arrays grow and shrink automatically
- Mixed types: Can contain any data type
- Sparse arrays: Can have gaps (undefined elements)
- Time complexity: Access O(1), Search O(n), Insert/Delete at end O(1), Insert/Delete at beginning O(n)
When to Use:
- ✅ Ordered collections where order matters
- ✅ When you need indexed access
- ✅ Collections that need frequent iteration
- ❌ Avoid for frequent insertions/deletions at the beginning (use linked lists instead)
Objects (Hash Maps) 🗺️
Objects in JavaScript are essentially hash maps—key-value pairs where keys are strings (or Symbols) and values can be any type.
// Creating objectsconst user = { name: "John", age: 30, email: "john@example.com",};
const userMap = new Object();userMap["key"] = "value";
// Object operationsuser.name; // Access: O(1) averageuser["age"] = 31; // Set: O(1) averagedelete user.email; // Delete: O(1) average"name" in user; // Check existence: O(1) averageObject.keys(user); // Get all keys: O(n)
// Object methodsconst entries = Object.entries(user); // [['name', 'John'], ...]const values = Object.values(user); // ['John', 30, ...]const keys = Object.keys(user); // ['name', 'age', ...]Key Characteristics:
- Fast lookups: Average O(1) for access, insertion, deletion
- Unordered: Property order not guaranteed (though ES2015+ maintains insertion order for string keys)
- Flexible keys: Strings, Symbols, or numbers (converted to strings)
- Prototype chain: Can inherit properties from prototype
When to Use:
- ✅ Key-value mappings with string keys
- ✅ When you need fast lookups by key
- ✅ Representing entities (users, products, etc.)
- ❌ Avoid when you need ordered iteration (use Map instead)
- ❌ Avoid when keys might not be strings (use Map instead)
Maps 🗺️
Maps are collections of key-value pairs similar to objects, but with important differences that make them more suitable for certain use cases.
// Creating Mapsconst map = new Map();map.set("name", "John");map.set(1, "one");map.set(true, "boolean key");
// Map operationsmap.get("name"); // Access: O(1) averagemap.has("name"); // Check existence: O(1) averagemap.delete("name"); // Delete: O(1) averagemap.size; // Get size: O(1)map.clear(); // Clear all: O(n)
// Iterationfor (const [key, value] of map) { console.log(key, value);}
map.forEach((value, key) => { console.log(key, value);});
// Converting from arraysconst entries = [ ["a", 1], ["b", 2],];const mapFromArray = new Map(entries);Key Advantages over Objects:
- ✅ Any type can be a key (objects, functions, primitives)
- ✅ Maintains insertion order
- ✅ Size property (no need for Object.keys().length)
- ✅ Better performance for frequent additions/deletions
- ✅ No prototype pollution
- ✅ Direct iteration support
When to Use:
- ✅ When keys are not strings
- ✅ When you need ordered key-value pairs
- ✅ When you frequently add/remove key-value pairs
- ✅ When you need to use objects as keys
Sets 📦
Sets are collections of unique values. Each value can only occur once in a Set.
// Creating Setsconst set = new Set();set.add(1);set.add(2);set.add(2); // Duplicate, ignored
const setFromArray = new Set([1, 2, 3, 3, 4]); // {1, 2, 3, 4}
// Set operationsset.has(1); // Check existence: O(1) averageset.delete(1); // Delete: O(1) averageset.size; // Get size: O(1)set.clear(); // Clear all: O(n)
// Set methodsconst union = new Set([...set1, ...set2]);const intersection = new Set([...set1].filter((x) => set2.has(x)));const difference = new Set([...set1].filter((x) => !set2.has(x)));
// Converting to arrayconst array = Array.from(set);const array2 = [...set];Key Characteristics:
- Uniqueness: Automatically handles duplicate values
- Fast lookups: O(1) average for has(), add(), delete()
- Value equality: Uses SameValueZero comparison (NaN === NaN)
When to Use:
- ✅ Removing duplicates from arrays
- ✅ Fast membership testing
- ✅ Mathematical set operations (union, intersection, difference)
- ✅ Tracking unique items
WeakMap and WeakSet 🔗
WeakMap and WeakSet are variants that hold “weak” references, allowing garbage collection when the key/value is no longer referenced elsewhere.
// WeakMap - keys must be objectsconst weakMap = new WeakMap();const obj = {};weakMap.set(obj, "value");weakMap.get(obj); // 'value'
// WeakSet - values must be objectsconst weakSet = new WeakSet();weakSet.add(obj);weakSet.has(obj); // true
// When obj is garbage collected, entries are automatically removedKey Characteristics:
- Weak references: Don’t prevent garbage collection
- No iteration: Cannot iterate over WeakMap/WeakSet
- No size property: Cannot check size
- Object keys only: WeakMap keys must be objects
When to Use:
- ✅ Storing private data for objects
- ✅ Caching computed values tied to object lifecycle
- ✅ Tracking object metadata without preventing GC
- ✅ DOM element metadata
Custom Data Structures
While JavaScript’s built-in structures cover many use cases, some problems require custom data structures. Let’s implement the most common ones.
Stacks 📚
A stack is a Last-In-First-Out (LIFO) data structure. Think of it like a stack of plates—you add to the top and remove from the top.
class Stack { constructor() { this.items = []; }
// Add element to top of stack: O(1) push(element) { this.items.push(element); }
// Remove and return top element: O(1) pop() { if (this.isEmpty()) { return null; } return this.items.pop(); }
// Return top element without removing: O(1) peek() { if (this.isEmpty()) { return null; } return this.items[this.items.length - 1]; }
// Check if stack is empty: O(1) isEmpty() { return this.items.length === 0; }
// Get stack size: O(1) size() { return this.items.length; }
// Clear stack: O(1) clear() { this.items = []; }}
// Usage exampleconst stack = new Stack();stack.push(1);stack.push(2);stack.push(3);console.log(stack.pop()); // 3console.log(stack.peek()); // 2Common Use Cases:
- Function call stack (handled by JavaScript engine)
- Undo/redo functionality
- Expression evaluation (infix to postfix conversion)
- Browser history navigation
- Balanced parentheses checking
Example: Balanced Parentheses Checker
function isBalanced(str) { const stack = new Stack(); const pairs = { "(": ")", "[": "]", "{": "}", };
for (const char of str) { if (pairs[char]) { // Opening bracket stack.push(char); } else if (Object.values(pairs).includes(char)) { // Closing bracket if (stack.isEmpty() || pairs[stack.pop()] !== char) { return false; } } }
return stack.isEmpty();}
console.log(isBalanced("()[]{}")); // trueconsole.log(isBalanced("([)]")); // falseQueues 🎫
A queue is a First-In-First-Out (FIFO) data structure. Think of it like a line at a store—first person in is first person out.
class Queue { constructor() { this.items = []; }
// Add element to end of queue: O(1) enqueue(element) { this.items.push(element); }
// Remove and return front element: O(n) - inefficient! dequeue() { if (this.isEmpty()) { return null; } return this.items.shift(); // O(n) operation }
// Return front element without removing: O(1) front() { if (this.isEmpty()) { return null; } return this.items[0]; }
isEmpty() { return this.items.length === 0; }
size() { return this.items.length; }}Optimized Queue Implementation 💡
The above implementation has O(n) dequeue because shift() moves all elements. Here’s an optimized version:
class OptimizedQueue { constructor() { this.items = {}; this.frontIndex = 0; this.backIndex = 0; }
// Add element: O(1) enqueue(element) { this.items[this.backIndex] = element; this.backIndex++; }
// Remove element: O(1) dequeue() { if (this.isEmpty()) { return null; } const item = this.items[this.frontIndex]; delete this.items[this.frontIndex]; this.frontIndex++; return item; }
front() { if (this.isEmpty()) { return null; } return this.items[this.frontIndex]; }
isEmpty() { return this.frontIndex === this.backIndex; }
size() { return this.backIndex - this.frontIndex; }}Common Use Cases:
- Task scheduling
- Breadth-first search (BFS)
- Print queue management
- Message queues
- Request handling in web servers
Priority Queues 🎯
A priority queue is a queue where elements are served based on priority rather than insertion order.
class PriorityQueue { constructor() { this.items = []; }
// Add element with priority: O(n) - could be optimized with heap enqueue(element, priority) { const queueElement = { element, priority };
if (this.isEmpty()) { this.items.push(queueElement); } else { let added = false; for (let i = 0; i < this.items.length; i++) { if (priority < this.items[i].priority) { this.items.splice(i, 0, queueElement); added = true; break; } } if (!added) { this.items.push(queueElement); } } }
dequeue() { if (this.isEmpty()) { return null; } return this.items.shift().element; }
isEmpty() { return this.items.length === 0; }}
// Usageconst pq = new PriorityQueue();pq.enqueue("Task 1", 3);pq.enqueue("Task 2", 1); // Higher prioritypq.enqueue("Task 3", 2);console.log(pq.dequeue()); // 'Task 2' (priority 1)Linked Lists 🔗
A linked list is a linear data structure where elements are stored in nodes, and each node points to the next node.
class ListNode { constructor(value) { this.value = value; this.next = null; }}
class LinkedList { constructor() { this.head = null; this.length = 0; }
// Add to end: O(n) append(value) { const newNode = new ListNode(value);
if (!this.head) { this.head = newNode; } else { let current = this.head; while (current.next) { current = current.next; } current.next = newNode; } this.length++; }
// Add to beginning: O(1) prepend(value) { const newNode = new ListNode(value); newNode.next = this.head; this.head = newNode; this.length++; }
// Insert at index: O(n) insertAt(index, value) { if (index < 0 || index > this.length) { return false; }
if (index === 0) { this.prepend(value); return true; }
const newNode = new ListNode(value); let current = this.head; for (let i = 0; i < index - 1; i++) { current = current.next; } newNode.next = current.next; current.next = newNode; this.length++; return true; }
// Remove by value: O(n) remove(value) { if (!this.head) { return false; }
if (this.head.value === value) { this.head = this.head.next; this.length--; return true; }
let current = this.head; while (current.next && current.next.value !== value) { current = current.next; }
if (current.next) { current.next = current.next.next; this.length--; return true; }
return false; }
// Find value: O(n) find(value) { let current = this.head; while (current) { if (current.value === value) { return current; } current = current.next; } return null; }
// Convert to array: O(n) toArray() { const result = []; let current = this.head; while (current) { result.push(current.value); current = current.next; } return result; }}
// Usageconst list = new LinkedList();list.append(1);list.append(2);list.append(3);list.prepend(0);console.log(list.toArray()); // [0, 1, 2, 3]Doubly Linked List 🔄
A doubly linked list has nodes that point to both next and previous nodes:
class DoublyListNode { constructor(value) { this.value = value; this.next = null; this.prev = null; }}
class DoublyLinkedList { constructor() { this.head = null; this.tail = null; this.length = 0; }
append(value) { const newNode = new DoublyListNode(value);
if (!this.head) { this.head = newNode; this.tail = newNode; } else { newNode.prev = this.tail; this.tail.next = newNode; this.tail = newNode; } this.length++; }
// Can traverse backwards efficiently // Other methods similar but update both next and prev pointers}When to Use:
- ✅ Frequent insertions/deletions in the middle
- ✅ Unknown or dynamic size
- ✅ Implementing stacks/queues
- ❌ Avoid when you need random access (use arrays)
Trees 🌳
Trees are hierarchical data structures with a root node and child nodes. Each node can have multiple children.
class TreeNode { constructor(value) { this.value = value; this.children = []; }
addChild(childNode) { this.children.push(childNode); }}
class Tree { constructor(rootValue) { this.root = new TreeNode(rootValue); }
// Depth-First Search (DFS) traverseDFS(callback) { function traverse(node) { callback(node.value); node.children.forEach((child) => traverse(child)); } traverse(this.root); }
// Breadth-First Search (BFS) traverseBFS(callback) { const queue = [this.root]; while (queue.length > 0) { const node = queue.shift(); callback(node.value); queue.push(...node.children); } }}Binary Search Tree (BST) 🔍
A BST is a tree where each node has at most two children, and values are ordered (left < parent < right):
class BSTNode { constructor(value) { this.value = value; this.left = null; this.right = null; }}
class BinarySearchTree { constructor() { this.root = null; }
// Insert: O(log n) average, O(n) worst case insert(value) { const newNode = new BSTNode(value);
if (!this.root) { this.root = newNode; return this; }
let current = this.root; while (true) { if (value === current.value) { return undefined; // Duplicate } if (value < current.value) { if (!current.left) { current.left = newNode; return this; } current = current.left; } else { if (!current.right) { current.right = newNode; return this; } current = current.right; } } }
// Find: O(log n) average, O(n) worst case find(value) { if (!this.root) { return false; }
let current = this.root; while (current) { if (value === current.value) { return true; } if (value < current.value) { current = current.left; } else { current = current.right; } } return false; }
// In-order traversal: left, root, right inOrder() { const result = []; function traverse(node) { if (node.left) traverse(node.left); result.push(node.value); if (node.right) traverse(node.right); } traverse(this.root); return result; }
// Pre-order traversal: root, left, right preOrder() { const result = []; function traverse(node) { result.push(node.value); if (node.left) traverse(node.left); if (node.right) traverse(node.right); } traverse(this.root); return result; }
// Post-order traversal: left, right, root postOrder() { const result = []; function traverse(node) { if (node.left) traverse(node.left); if (node.right) traverse(node.right); result.push(node.value); } traverse(this.root); return result; }}
// Usageconst bst = new BinarySearchTree();bst.insert(10);bst.insert(5);bst.insert(15);bst.insert(3);bst.insert(7);console.log(bst.inOrder()); // [3, 5, 7, 10, 15]When to Use:
- ✅ Hierarchical data (file systems, organization charts)
- ✅ Search operations (BST)
- ✅ Expression parsing
- ✅ Decision trees
- ✅ Database indexing
Graphs 🕸️
Graphs are collections of nodes (vertices) connected by edges. They can be directed or undirected, weighted or unweighted.
class Graph { constructor() { this.adjacencyList = {}; }
// Add vertex: O(1) addVertex(vertex) { if (!this.adjacencyList[vertex]) { this.adjacencyList[vertex] = []; } }
// Add edge: O(1) addEdge(vertex1, vertex2) { this.adjacencyList[vertex1].push(vertex2); this.adjacencyList[vertex2].push(vertex1); // Undirected graph }
// Remove edge: O(V) removeEdge(vertex1, vertex2) { this.adjacencyList[vertex1] = this.adjacencyList[vertex1].filter( (v) => v !== vertex2, ); this.adjacencyList[vertex2] = this.adjacencyList[vertex2].filter( (v) => v !== vertex1, ); }
// Depth-First Search (DFS): O(V + E) dfs(start) { const result = []; const visited = {}; const adjacencyList = this.adjacencyList;
(function dfsHelper(vertex) { if (!vertex) return null; visited[vertex] = true; result.push(vertex); adjacencyList[vertex].forEach((neighbor) => { if (!visited[neighbor]) { return dfsHelper(neighbor); } }); })(start);
return result; }
// Breadth-First Search (BFS): O(V + E) bfs(start) { const queue = [start]; const result = []; const visited = {}; visited[start] = true;
while (queue.length) { const vertex = queue.shift(); result.push(vertex);
this.adjacencyList[vertex].forEach((neighbor) => { if (!visited[neighbor]) { visited[neighbor] = true; queue.push(neighbor); } }); }
return result; }}
// Usageconst graph = new Graph();graph.addVertex("A");graph.addVertex("B");graph.addVertex("C");graph.addEdge("A", "B");graph.addEdge("A", "C");graph.addEdge("B", "C");console.log(graph.dfs("A")); // ['A', 'B', 'C']When to Use:
- ✅ Social networks
- ✅ Navigation systems
- ✅ Recommendation systems
- ✅ Network routing
- ✅ Dependency resolution
Hash Tables 🗄️
Hash tables provide fast key-value access using a hash function. JavaScript’s Objects and Maps are essentially hash tables.
class HashTable { constructor(size = 53) { this.keyMap = new Array(size); }
// Simple hash function _hash(key) { let total = 0; const WEIRD_PRIME = 31; for (let i = 0; i < Math.min(key.length, 100); i++) { const char = key[i]; const value = char.charCodeAt(0) - 96; total = (total * WEIRD_PRIME + value) % this.keyMap.length; } return total; }
// Set key-value pair: O(1) average set(key, value) { const index = this._hash(key); if (!this.keyMap[index]) { this.keyMap[index] = []; } // Handle collisions with separate chaining const existingPair = this.keyMap[index].find((pair) => pair[0] === key); if (existingPair) { existingPair[1] = value; // Update existing } else { this.keyMap[index].push([key, value]); } }
// Get value by key: O(1) average get(key) { const index = this._hash(key); if (this.keyMap[index]) { const pair = this.keyMap[index].find((pair) => pair[0] === key); return pair ? pair[1] : undefined; } return undefined; }
// Get all keys: O(n) keys() { const keysArr = []; for (let i = 0; i < this.keyMap.length; i++) { if (this.keyMap[i]) { for (let j = 0; j < this.keyMap[i].length; j++) { if (!keysArr.includes(this.keyMap[i][j][0])) { keysArr.push(this.keyMap[i][j][0]); } } } } return keysArr; }
// Get all values: O(n) values() { const valuesArr = []; for (let i = 0; i < this.keyMap.length; i++) { if (this.keyMap[i]) { for (let j = 0; j < this.keyMap[i].length; j++) { if (!valuesArr.includes(this.keyMap[i][j][1])) { valuesArr.push(this.keyMap[i][j][1]); } } } } return valuesArr; }}
// Usageconst ht = new HashTable();ht.set("name", "John");ht.set("age", 30);console.log(ht.get("name")); // 'John'Collision Handling:
- Separate Chaining: Store multiple key-value pairs at each index (used above)
- Linear Probing: Find next available slot when collision occurs
When to Use:
- ✅ Fast lookups, insertions, deletions
- ✅ Caching
- ✅ Database indexing
- ✅ Counting frequencies
- ✅ JavaScript Objects and Maps are hash tables!
Choosing the Right Data Structure
Selecting the appropriate data structure depends on your specific requirements. Here’s a decision guide:
Access Patterns
| Requirement | Recommended Structure | Why |
|---|---|---|
| Random access by index | Array | O(1) indexed access |
| Random access by key | Object/Map | O(1) key lookup |
| Sequential access only | Linked List | Efficient insertion/deletion |
| LIFO operations | Stack | Natural fit for last-in-first-out |
| FIFO operations | Queue | Natural fit for first-in-first-out |
Operation Frequencies
// ✅ Frequent insertions/deletions at beginning// Use: Linked Listconst list = new LinkedList();list.prepend(item); // O(1)
// ❌ Don't use: Arrayconst arr = [];arr.unshift(item); // O(n) - inefficient!
// ✅ Frequent lookups by key// Use: Object/Mapconst map = new Map();map.get(key); // O(1)
// ❌ Don't use: Arrayconst arr = [];arr.find((item) => item.key === key); // O(n) - inefficient!
// ✅ Need unique values// Use: Setconst set = new Set([1, 2, 2, 3]); // {1, 2, 3}
// ❌ Don't use: Array (manual deduplication needed)const arr = [1, 2, 2, 3];const unique = [...new Set(arr)]; // Extra step neededSize Considerations
- Fixed/known size: Arrays are efficient
- Dynamic/unknown size: Linked Lists, Trees, or dynamic arrays
- Very large datasets: Consider memory-efficient structures
Common Scenarios
Scenario 1: Implementing Undo/Redo
// ✅ Use: Stackconst undoStack = new Stack();const redoStack = new Stack();
function performAction(action) { undoStack.push(action); redoStack.clear();}
function undo() { const action = undoStack.pop(); if (action) { redoStack.push(action); // Reverse action }}Scenario 2: Task Scheduler
// ✅ Use: Priority Queueconst scheduler = new PriorityQueue();scheduler.enqueue("High priority task", 1);scheduler.enqueue("Low priority task", 5);const nextTask = scheduler.dequeue(); // Gets highest priorityScenario 3: Social Network Friend Suggestions
// ✅ Use: Graphconst socialGraph = new Graph();// Add users as vertices// Add friendships as edges// Use BFS to find friends of friendsPerformance Analysis
Understanding time and space complexity helps you choose the right data structure and optimize your code.
Time Complexity Comparison
| Operation | Array | Object/Map | Set | Linked List | BST | Hash Table |
|---|---|---|---|---|---|---|
| Access | O(1) | O(1) | O(1) | O(n) | O(log n) | O(1) |
| Search | O(n) | O(1) | O(1) | O(n) | O(log n) | O(1) |
| Insertion (end) | O(1) | O(1) | O(1) | O(1) | O(log n) | O(1) |
| Insertion (beginning) | O(n) | N/A | N/A | O(1) | O(log n) | N/A |
| Deletion | O(n) | O(1) | O(1) | O(n) | O(log n) | O(1) |
Space Complexity
- Arrays: O(n) - stores n elements
- Objects/Maps: O(n) - stores n key-value pairs
- Linked Lists: O(n) - stores n nodes plus pointers
- Trees: O(n) - stores n nodes plus child pointers
- Graphs: O(V + E) - V vertices, E edges
Measuring Performance 🔍
// Measure execution timefunction measureTime(fn) { const start = performance.now(); fn(); const end = performance.now(); return end - start;}
// Compare array vs Set for membership testingconst arr = Array.from({ length: 1000000 }, (_, i) => i);const set = new Set(arr);
const searchValue = 999999;
const arrayTime = measureTime(() => { arr.includes(searchValue); // O(n)});
const setTime = measureTime(() => { set.has(searchValue); // O(1)});
console.log(`Array: ${arrayTime}ms`);console.log(`Set: ${setTime}ms`);// Set will be significantly faster for large datasetsOptimization Tips 💡
Tip 1: Use Sets for Membership Testing
// ❌ Slow: O(n)const items = [1, 2, 3, 4, 5];if (items.includes(3)) { /* ... */}
// ✅ Fast: O(1)const itemsSet = new Set([1, 2, 3, 4, 5]);if (itemsSet.has(3)) { /* ... */}Tip 2: Prefer Maps for Frequent Additions/Deletions
// ❌ Slower for frequent operationsconst obj = {};obj[key] = value;delete obj[key];
// ✅ Faster for frequent operationsconst map = new Map();map.set(key, value);map.delete(key);Tip 3: Use Array Methods Efficiently
// ❌ Multiple passes: O(2n)const doubled = arr.map((x) => x * 2);const filtered = doubled.filter((x) => x > 10);
// ✅ Single pass: O(n)const result = arr.reduce((acc, x) => { const doubled = x * 2; if (doubled > 10) acc.push(doubled); return acc;}, []);Real-World Applications
Let’s explore practical applications of data structures in real JavaScript applications.
Application 1: Browser History (Stack)
class BrowserHistory { constructor() { this.backStack = new Stack(); this.forwardStack = new Stack(); this.current = null; }
visit(url) { this.backStack.push(this.current); this.current = url; this.forwardStack.clear(); }
back() { if (!this.backStack.isEmpty()) { this.forwardStack.push(this.current); this.current = this.backStack.pop(); return this.current; } return null; }
forward() { if (!this.forwardStack.isEmpty()) { this.backStack.push(this.current); this.current = this.forwardStack.pop(); return this.current; } return null; }}Application 2: LRU Cache (Map + Doubly Linked List)
class LRUCache { constructor(capacity) { this.capacity = capacity; this.cache = new Map(); }
get(key) { if (!this.cache.has(key)) { return -1; } // Move to end (most recently used) const value = this.cache.get(key); this.cache.delete(key); this.cache.set(key, value); return value; }
put(key, value) { if (this.cache.has(key)) { this.cache.delete(key); } else if (this.cache.size >= this.capacity) { // Remove least recently used (first item) const firstKey = this.cache.keys().next().value; this.cache.delete(firstKey); } this.cache.set(key, value); }}Application 3: Expression Evaluator (Stack)
function evaluatePostfix(expression) { const stack = new Stack(); const tokens = expression.split(" ");
for (const token of tokens) { if (!isNaN(token)) { // Number stack.push(parseFloat(token)); } else { // Operator const b = stack.pop(); const a = stack.pop(); let result;
switch (token) { case "+": result = a + b; break; case "-": result = a - b; break; case "*": result = a * b; break; case "/": result = a / b; break; default: throw new Error(`Unknown operator: ${token}`); }
stack.push(result); } }
return stack.pop();}
// Usage: "3 4 + 2 *" = (3 + 4) * 2 = 14console.log(evaluatePostfix("3 4 + 2 *")); // 14Application 4: Autocomplete with Trie
class TrieNode { constructor() { this.children = {}; this.isEndOfWord = false; }}
class Trie { constructor() { this.root = new TrieNode(); }
insert(word) { let current = this.root; for (const char of word) { if (!current.children[char]) { current.children[char] = new TrieNode(); } current = current.children[char]; } current.isEndOfWord = true; }
search(word) { let current = this.root; for (const char of word) { if (!current.children[char]) { return false; } current = current.children[char]; } return current.isEndOfWord; }
startsWith(prefix) { let current = this.root; for (const char of prefix) { if (!current.children[char]) { return []; } current = current.children[char]; } return this._getAllWords(current, prefix); }
_getAllWords(node, prefix) { const words = []; if (node.isEndOfWord) { words.push(prefix); } for (const [char, childNode] of Object.entries(node.children)) { words.push(...this._getAllWords(childNode, prefix + char)); } return words; }}
// Usage for autocompleteconst trie = new Trie();trie.insert("javascript");trie.insert("java");trie.insert("python");console.log(trie.startsWith("jav")); // ['java', 'javascript']Best Practices
Follow these best practices to use data structures effectively in your JavaScript code.
1. Choose the Right Tool for the Job ✅
// ✅ Use Set for unique valuesconst uniqueIds = new Set(userIds);
// ❌ Don't manually check duplicates in arrayconst uniqueIds = [];userIds.forEach((id) => { if (!uniqueIds.includes(id)) { // O(n) check! uniqueIds.push(id); }});2. Understand Time Complexity ⚠️
Always consider the time complexity of operations, especially in loops:
// ❌ O(n²) - nested includesfunction hasDuplicates(arr) { for (let i = 0; i < arr.length; i++) { if (arr.includes(arr[i], i + 1)) { // O(n) inside O(n) loop return true; } } return false;}
// ✅ O(n) - Set lookupfunction hasDuplicates(arr) { const seen = new Set(); for (const item of arr) { if (seen.has(item)) { // O(1) return true; } seen.add(item); } return false;}3. Use Modern JavaScript Features 💡
Leverage ES6+ features for cleaner code:
// ✅ Use Map for object-like structures with non-string keysconst nodeMap = new Map();nodeMap.set(node1, "value");
// ✅ Use Set for mathematical operationsconst set1 = new Set([1, 2, 3]);const set2 = new Set([2, 3, 4]);const intersection = new Set([...set1].filter((x) => set2.has(x)));
// ✅ Use destructuring with Mapsfor (const [key, value] of map) { console.log(key, value);}4. Handle Edge Cases 🔍
Always consider edge cases:
class Stack { pop() { // ✅ Check for empty stack if (this.isEmpty()) { throw new Error("Stack is empty"); // or return null/undefined } return this.items.pop(); }}
// ✅ Validate inputfunction insertIntoBST(root, value) { if (value === null || value === undefined) { throw new Error("Value cannot be null"); } // ... insertion logic}5. Optimize for Your Use Case 🚀
// If you need frequent insertions at both ends, use Dequeclass Deque { constructor() { this.items = {}; this.front = 0; this.back = 0; }
addFront(item) { this.front--; this.items[this.front] = item; }
addBack(item) { this.items[this.back] = item; this.back++; }
removeFront() { const item = this.items[this.front]; delete this.items[this.front]; this.front++; return item; }
removeBack() { this.back--; const item = this.items[this.back]; delete this.items[this.back]; return item; }}6. Document Your Choices 📝
When implementing custom data structures, document why you chose them:
/** * Uses a Stack for O(1) push/pop operations. * Alternative: Array with push/pop (same performance, but Stack * provides clearer intent and prevents accidental index access). */class UndoManager { constructor() { this.undoStack = new Stack(); } // ...}7. Test Your Implementations 🧪
Always test your data structure implementations:
function testStack() { const stack = new Stack();
// Test push stack.push(1); console.assert(stack.size() === 1, "Push failed");
// Test peek console.assert(stack.peek() === 1, "Peek failed");
// Test pop console.assert(stack.pop() === 1, "Pop failed"); console.assert(stack.isEmpty(), "Stack should be empty");
console.log("All tests passed!");}Conclusion
Data structures are the foundation of efficient programming. JavaScript provides excellent built-in structures like Arrays, Objects, Maps, and Sets, but understanding when to use custom implementations like Stacks, Queues, Trees, and Graphs will make you a better developer.
Key Takeaways:
- Built-in structures are powerful: JavaScript’s native data structures cover most use cases efficiently
- Choose based on operations: Consider what operations you’ll perform most frequently
- Understand complexity: Time and space complexity matter, especially at scale
- Custom structures have their place: Some problems require specialized data structures
- Practice makes perfect: Implement these structures yourself to truly understand them
Next Steps:
- Practice implementing these data structures from scratch
- Solve algorithm problems on platforms like LeetCode or HackerRank
- Analyze the performance of data structures in your own projects
- Explore advanced structures like AVL trees, red-black trees, and heaps
- Learn about graph algorithms like Dijkstra’s and A* search
For more JavaScript fundamentals, check out our guides on JavaScript Promises and Async/Await, Understanding JavaScript Closures, and Modern JavaScript Features. You might also find our JavaScript Array Methods and JavaScript Object Methods cheatsheets helpful for working with built-in data structures.
Remember, the best data structure is the one that makes your code readable, maintainable, and performant for your specific use case. Start with built-in structures, and only implement custom ones when you have a clear performance or functionality need.