Data Structures in JavaScript

Learn the most important data structures in JavaScript with clear explanations and practical examples. Understand arrays, linked lists, stacks, queues, trees, graphs and hash tables.

Published on 04 June 2026
Reading Time 9
Number of Words 2003

Data Structures in JavaScript

Data structures are ways to organize and store data so that operations (like insertion, deletion, search, traversal) can be performed efficiently. In JavaScript, built-in structures like arrays and objects are useful, but understanding custom structures (like linked lists, stacks, queues, trees, graphs, hash tables) lets you solve complex problems with more control.

Below is a structured walkthrough of key data structures, with pros, use cases, and JavaScript-based examples.


Table of Contents

  1. Why Data Structures Matter
  2. Built-in Structures (Arrays, Objects, Map, Set)
  3. Custom Data Structures
    • Linked List
    • Stack
    • Queue
    • Tree (Binary Tree, BST)
    • Graph
    • Hash Table / Hash Map
  4. When & Why to Use Each
  5. Example Implementations in JavaScript
  6. Performance Considerations
  7. Tips & Best Practices


1. Why Data Structures Matter

Good data structures make your code:

  • Faster: operations like lookup, insertion, deletion are optimized
  • Cleaner: logic becomes more intuitive
  • Scalable: performance holds up with larger datasets
  • Maintainable: structure makes code easier to reason about

Without proper structures, you may end up with convoluted or inefficient solutions.


2. Built-in Structures

Arrays

  • Ordered list of elements
  • Access by index (arr[0])
  • Useful methods: push, pop, shift, unshift, splice, slice, map, filter, reduce

Example – deduplicating via Set + array:

const arr = [1, 2, 3, 2, 1, 4];
const unique = [...new Set(arr)];
console.log(unique);  // [1, 2, 3, 4]

Arrays are versatile, but some operations (like inserting or deleting at front) can be costly (O(n)).

Objects, Map, Set

  • Object: key-value store (string or symbol keys)
  • Map: key-value store with any type keys, predictable iteration order
  • Set: collection of unique values

const map = new Map();
map.set('a', 1);
map.set(2, 'two');
console.log(map.get('a'));  // 1
console.log(map.get(2));    // "two"

These are great for dictionaries, caches, or sets of unique data.


3. Custom Data Structures

Linked List

A sequence of nodes where each node stores a value and a pointer/reference to the next node.

class ListNode {
  constructor(value) {
    this.value = value;
    this.next = null;
  }
}

class LinkedList {
  constructor() {
    this.head = null;
    this.tail = null;
    this.length = 0;
  }

  append(value) {
    const newNode = new ListNode(value);
    if (!this.head) {
      this.head = newNode;
      this.tail = newNode;
    } else {
      this.tail.next = newNode;
      this.tail = newNode;
    }
    this.length++;
  }

  prepend(value) {
    const newNode = new ListNode(value);
    if (!this.head) {
      this.head = newNode;
      this.tail = newNode;
    } else {
      newNode.next = this.head;
      this.head = newNode;
    }
    this.length++;
  }

  insert(index, value) {
    if (index <= 0) {
      this.prepend(value);
      return;
    }
    if (index >= this.length) {
      this.append(value);
      return;
    }
    const newNode = new ListNode(value);
    let curr = this.head;
    for (let i = 0; i < index - 1; i++) {
      curr = curr.next;
    }
    newNode.next = curr.next;
    curr.next = newNode;
    this.length++;
  }

  remove(index) {
    if (!this.head) return null;
    if (index <= 0) {
      const removed = this.head;
      this.head = this.head.next;
      this.length--;
      if (!this.head) this.tail = null;
      return removed.value;
    }
    let curr = this.head;
    for (let i = 0; i < index - 1; i++) {
      curr = curr.next;
    }
    const removed = curr.next;
    curr.next = removed.next;
    if (removed === this.tail) {
      this.tail = curr;
    }
    this.length--;
    return removed.value;
  }

  toArray() {
    const arr = [];
    let curr = this.head;
    while (curr) {
      arr.push(curr.value);
      curr = curr.next;
    }
    return arr;
  }
}

// Demo:
const list = new LinkedList();
list.append(10);
list.append(20);
list.prepend(5);
list.insert(1, 7);
console.log(list.toArray());   // [5, 7, 10, 20]
list.remove(2);
console.log(list.toArray());   // [5, 7, 20]


Stack & Queue

We already improved on stacks and queues earlier, so see those versions. (Stack = LIFO, Queue = FIFO)


Tree & Binary Search Tree (BST)

A tree is a hierarchical structure. A binary tree is a tree where each node has up to two children. A binary search tree maintains the property: left subtree values < node < right subtree values.

class TreeNode {
  constructor(value) {
    this.value = value;
    this.left = null;
    this.right = null;
  }
}

class BST {
  constructor() {
    this.root = null;
  }

  insert(value) {
    const node = new TreeNode(value);
    if (!this.root) {
      this.root = node;
      return;
    }
    let curr = this.root;
    while (true) {
      if (value < curr.value) {
        if (!curr.left) {
          curr.left = node;
          return;
        }
        curr = curr.left;
      } else {
        if (!curr.right) {
          curr.right = node;
          return;
        }
        curr = curr.right;
      }
    }
  }

  contains(value) {
    let curr = this.root;
    while (curr) {
      if (value === curr.value) return true;
      curr = value < curr.value ? curr.left : curr.right;
    }
    return false;
  }

  // Traversals
  inOrder(node = this.root, arr = []) {
    if (node) {
      this.inOrder(node.left, arr);
      arr.push(node.value);
      this.inOrder(node.right, arr);
    }
    return arr;
  }

  preOrder(node = this.root, arr = []) {
    if (node) {
      arr.push(node.value);
      this.preOrder(node.left, arr);
      this.preOrder(node.right, arr);
    }
    return arr;
  }

  postOrder(node = this.root, arr = []) {
    if (node) {
      this.postOrder(node.left, arr);
      this.postOrder(node.right, arr);
      arr.push(node.value);
    }
    return arr;
  }
}

// Demo:
const bst = new BST();
bst.insert(10);
bst.insert(5);
bst.insert(15);
bst.insert(7);
console.log(bst.contains(7));         // true
console.log(bst.contains(3));         // false
console.log(bst.inOrder());           // [5, 7, 10, 15]
console.log(bst.preOrder());          // [10, 5, 7, 15]
console.log(bst.postOrder());         // [7, 5, 15, 10]


Graph

A graph is a set of nodes (vertices) connected by edges. Edges may be directed or undirected. You can represent graphs by adjacency lists or matrices.

class Graph {
  constructor() {
    this.adjList = new Map();
  }

  addVertex(v) {
    if (!this.adjList.has(v)) {
      this.adjList.set(v, []);
    }
  }

  addEdge(v, w) {
    this.addVertex(v);
    this.addVertex(w);
    this.adjList.get(v).push(w);
    this.adjList.get(w).push(v);  // undirected
  }

  // BFS traversal
  bfs(start) {
    const visited = new Set();
    const queue = [start];
    const result = [];

    visited.add(start);
    while (queue.length > 0) {
      const v = queue.shift();
      result.push(v);

      for (const neighbor of this.adjList.get(v)) {
        if (!visited.has(neighbor)) {
          visited.add(neighbor);
          queue.push(neighbor);
        }
      }
    }
    return result;
  }
}

// Demo:
const graph = new Graph();
graph.addEdge('A', 'B');
graph.addEdge('A', 'C');
graph.addEdge('B', 'D');
graph.addEdge('C', 'E');
console.log(graph.bfs('A'));  // e.g. ["A", "B", "C", "D", "E"]


Hash Table / Hash Map

A hash table is a data structure that maps keys to values using a hash function. In JS, Map is a built-in hashed map. But you can build a simple hash map:

class HashMap {
  constructor(size = 42) {
    this.buckets = Array(size).fill(null).map(() => []);
  }

  hash(key) {
    let hash = 0;
    const str = String(key);
    for (let char of str) {
      hash = (hash * 31 + char.charCodeAt(0)) % this.buckets.length;
    }
    return hash;
  }

  set(key, value) {
    const idx = this.hash(key);
    const bucket = this.buckets[idx];
    for (const pair of bucket) {
      if (pair[0] === key) {
        pair[1] = value;
        return;
      }
    }
    bucket.push([key, value]);
  }

  get(key) {
    const idx = this.hash(key);
    const bucket = this.buckets[idx];
    for (const pair of bucket) {
      if (pair[0] === key) {
        return pair[1];
      }
    }
    return undefined;
  }

  has(key) {
    return this.get(key) !== undefined;
  }

  remove(key) {
    const idx = this.hash(key);
    const bucket = this.buckets[idx];
    for (let i = 0; i < bucket.length; i++) {
      if (bucket[i][0] === key) {
        bucket.splice(i, 1);
        return true;
      }
    }
    return false;
  }
}

// Demo:
const map2 = new HashMap();
map2.set('name', 'Alice');
map2.set('age', 30);
console.log(map2.get('name'));   // "Alice"
console.log(map2.has('age'));    // true
map2.remove('age');
console.log(map2.has('age'));    // false


4. When & Why to Use Each

Structure Strengths / Use Cases Limitations
Array Simple, random access, built-in methods Insert/delete in middle/front can be slow
Linked List Fast insertion/deletion at ends or middle No random access; more pointers
Stack / Queue Localized push/pop or FIFO behavior Limited access to middle elements
BST / Tree Hierarchical data, sorted operations Can degrade if unbalanced
Graph Modeling relationships (networks, dependencies) Complexity of algorithms, cycle detection
Hash Table Fast key-based lookup, flexible keys Collisions handling, hash function quality

Use the simplest structure that meets your performance and functional needs.


5. Performance Considerations

  • Understand time complexities (Big O): for insertion, deletion, lookup, traversal
  • Watch for worst-case scenarios (e.g. unbalanced tree, hash collisions)
  • Choose proper implementation: e.g. array vs linked list
  • Avoid memory leaks by cleaning references


6. Tips & Best Practices

  • Always test with edge cases: empty, single element, large data
  • Use existing structures (Map, Set, Array) when possible
  • Keep your interface simple and consistent
  • Encapsulate internal implementation details (don’t expose internals)
  • Add utility conversion methods (toArray, toString)
  • Add unit tests to ensure correctness