Skip to main content

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

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 arrays
const fruits = ["apple", "banana", "orange"];
const numbers = new Array(1, 2, 3);
const empty = [];
// Array operations
fruits.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 methods
const 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 objects
const user = {
name: "John",
age: 30,
email: "john@example.com",
};
const userMap = new Object();
userMap["key"] = "value";
// Object operations
user.name; // Access: O(1) average
user["age"] = 31; // Set: O(1) average
delete user.email; // Delete: O(1) average
"name" in user; // Check existence: O(1) average
Object.keys(user); // Get all keys: O(n)
// Object methods
const 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 Maps
const map = new Map();
map.set("name", "John");
map.set(1, "one");
map.set(true, "boolean key");
// Map operations
map.get("name"); // Access: O(1) average
map.has("name"); // Check existence: O(1) average
map.delete("name"); // Delete: O(1) average
map.size; // Get size: O(1)
map.clear(); // Clear all: O(n)
// Iteration
for (const [key, value] of map) {
console.log(key, value);
}
map.forEach((value, key) => {
console.log(key, value);
});
// Converting from arrays
const 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 Sets
const 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 operations
set.has(1); // Check existence: O(1) average
set.delete(1); // Delete: O(1) average
set.size; // Get size: O(1)
set.clear(); // Clear all: O(n)
// Set methods
const 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 array
const 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 objects
const weakMap = new WeakMap();
const obj = {};
weakMap.set(obj, "value");
weakMap.get(obj); // 'value'
// WeakSet - values must be objects
const weakSet = new WeakSet();
weakSet.add(obj);
weakSet.has(obj); // true
// When obj is garbage collected, entries are automatically removed

Key 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 example
const stack = new Stack();
stack.push(1);
stack.push(2);
stack.push(3);
console.log(stack.pop()); // 3
console.log(stack.peek()); // 2

Common 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("()[]{}")); // true
console.log(isBalanced("([)]")); // false

Queues 🎫

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;
}
}
// Usage
const pq = new PriorityQueue();
pq.enqueue("Task 1", 3);
pq.enqueue("Task 2", 1); // Higher priority
pq.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;
}
}
// Usage
const 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;
}
}
// Usage
const 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;
}
}
// Usage
const 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;
}
}
// Usage
const 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

RequirementRecommended StructureWhy
Random access by indexArrayO(1) indexed access
Random access by keyObject/MapO(1) key lookup
Sequential access onlyLinked ListEfficient insertion/deletion
LIFO operationsStackNatural fit for last-in-first-out
FIFO operationsQueueNatural fit for first-in-first-out

Operation Frequencies

// ✅ Frequent insertions/deletions at beginning
// Use: Linked List
const list = new LinkedList();
list.prepend(item); // O(1)
// ❌ Don't use: Array
const arr = [];
arr.unshift(item); // O(n) - inefficient!
// ✅ Frequent lookups by key
// Use: Object/Map
const map = new Map();
map.get(key); // O(1)
// ❌ Don't use: Array
const arr = [];
arr.find((item) => item.key === key); // O(n) - inefficient!
// ✅ Need unique values
// Use: Set
const 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 needed

Size 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: Stack
const 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 Queue
const scheduler = new PriorityQueue();
scheduler.enqueue("High priority task", 1);
scheduler.enqueue("Low priority task", 5);
const nextTask = scheduler.dequeue(); // Gets highest priority

Scenario 3: Social Network Friend Suggestions

// ✅ Use: Graph
const socialGraph = new Graph();
// Add users as vertices
// Add friendships as edges
// Use BFS to find friends of friends

Performance Analysis

Understanding time and space complexity helps you choose the right data structure and optimize your code.

Time Complexity Comparison

OperationArrayObject/MapSetLinked ListBSTHash Table
AccessO(1)O(1)O(1)O(n)O(log n)O(1)
SearchO(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/AN/AO(1)O(log n)N/A
DeletionO(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 time
function measureTime(fn) {
const start = performance.now();
fn();
const end = performance.now();
return end - start;
}
// Compare array vs Set for membership testing
const 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 datasets

Optimization 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 operations
const obj = {};
obj[key] = value;
delete obj[key];
// ✅ Faster for frequent operations
const 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 = 14
console.log(evaluatePostfix("3 4 + 2 *")); // 14

Application 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 autocomplete
const 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 values
const uniqueIds = new Set(userIds);
// ❌ Don't manually check duplicates in array
const 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 includes
function 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 lookup
function 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 keys
const nodeMap = new Map();
nodeMap.set(node1, "value");
// ✅ Use Set for mathematical operations
const 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 Maps
for (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 input
function 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 Deque
class 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:

  1. Built-in structures are powerful: JavaScript’s native data structures cover most use cases efficiently
  2. Choose based on operations: Consider what operations you’ll perform most frequently
  3. Understand complexity: Time and space complexity matter, especially at scale
  4. Custom structures have their place: Some problems require specialized data structures
  5. 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.