Last Updated: March 31, 2026
This chapter is a focused crash course on the JavaScript features you will use repeatedly while learning DSA and preparing for coding interviews. Instead of covering the entire language, we will concentrate only on the parts that matter for solving problems efficiently in interviews.
Here is something unique about JavaScript compared to Java, Python, C++, and C#: you do not need to import anything for DSA. Every data structure and utility function you need is globally available.
On LeetCode, your solution is a function. There is no class wrapping, no import statements, no #include directives. You write your function and submit. This is one of the reasons JavaScript is popular for coding interviews: zero boilerplate.
Here is what is available to you out of the box:
| Built-in | What It Provides | DSA Use Case |
|---|---|---|
Array | Dynamic array with rich methods | Primary data structure for everything |
Map | Hash map with any key type | Frequency counting, O(1) lookups |
Set | Hash set with any value type | Visited tracking, deduplication |
Object | Key-value pairs (string keys) | Simple frequency maps |
Math | max, min, floor, ceil, abs, sqrt, log2 | Utility calculations |
JSON | stringify, parse | Deep copy, composite keys |
Infinity / -Infinity | Global sentinel values | Min/max initialization |
But JavaScript also has notable gaps. These are data structures you will need in DSA that have no built-in equivalent:
| Not Built-in | Java / Python Equivalent | What You Must Do |
|---|---|---|
| Queue | Queue / deque | Use array with pointer, or linked list |
| Deque | ArrayDeque / deque | Use array (limited), or custom class |
| PriorityQueue | PriorityQueue / heapq | Implement a min-heap yourself |
| TreeMap / TreeSet | TreeMap / SortedList | Sorted array + binary search (limited) |
We will cover workarounds for each of these missing structures later in this chapter. For now, just know that JavaScript gives you less out of the box than Java or Python, but what it does give you is clean, concise, and fast to write.
JavaScript is dynamically typed. You do not declare types, you just assign values and the engine figures it out:
For variable declarations, follow one rule: use `const` by default, `let` when you need to reassign, never `var`. The var keyword has function scope (not block scope), which leads to subtle bugs in loops and conditionals. Both let and const are block-scoped, which is what you expect coming from any other language.
For DSA, you will use a small set of types:
| Type | Example | DSA Use Case |
|---|---|---|
number | 42, 3.14, Infinity | Everything numeric (no separate int/float!) |
string | "hello" | Immutable character sequences |
boolean | true, false | Visited arrays, flags |
null | null | Intentional absence (tree/linked list terminators) |
undefined | undefined | Uninitialized variables, missing properties |
bigint | 42n | Values exceeding 2^53 - 1 (rare in DSA) |
Now here is the most important thing to understand about JavaScript numbers: there is no integer type. Every number is an IEEE 754 64-bit floating-point double. The number 42 and the number 42.0 are the same value, the same type, stored the same way.
This has three critical implications for DSA:
1. Division always produces a float. Unlike Java where 7 / 2 gives 3, in JavaScript 7 / 2 gives 3.5. You must explicitly truncate:
2. Integer precision is limited to 2^53 - 1. The Number.MAX_SAFE_INTEGER is 9,007,199,254,740,991. Beyond this, integer arithmetic silently loses precision:
For most DSA problems where constraints are up to 10^9, this is not an issue. But for problems involving large prefix sums or multiplication of large values, be aware. If you truly need arbitrary precision, use BigInt:
3. Binary search midpoint does not overflow, but needs floor. Unlike Java where (left + right) can overflow, JavaScript numbers safely hold sums up to 2^53. But you need Math.floor because division produces a float:
When a problem says "return an integer," you still return a number in JavaScript. But always make sure your arithmetic produces an integer result. Using Math.floor in binary search and division is a habit that prevents bugs.
The ternary operator works the same as in other languages:
One more thing about types: the typeof operator has some notorious quirks:
The typeof null === "object" is a bug from the first version of JavaScript in 1995 that was never fixed because it would break existing code. You will not encounter this in DSA problems, but it is worth knowing for interview trivia questions.
Most operators work as you would expect, but a few deserve special attention for DSA.
Always use `===` and `!==` in JavaScript. The == operator performs type coercion, which creates surprising results:
There is one acceptable use of ==: checking for both null and undefined in a single comparison. val == null is true when val is either null or undefined, and false for everything else. Some codebases use this idiom, but val === null || val === undefined or val == null is a style choice. In DSA, just be consistent.
The % operator appears in hashing, cyclic array problems, and math-heavy questions. Like Java, JavaScript's % can return negative values for negative operands: -7 % 3 gives -1, not 2. To guarantee a positive result:
Many problems ask you to return the result "modulo 10^9 + 7" to prevent large outputs. This is a standard pattern:
Here is a critical JavaScript-specific gotcha: bitwise operators convert operands to 32-bit signed integers. This means:
This limits bitmask problems in JavaScript to 31 bits instead of the 32 you get in Java or C++. For most DSA problems, 31 bits (representing sets of up to 31 elements) is sufficient. But be aware of this if you are porting bitmask DP solutions from other languages.
The key bitwise operators:
| Operator | Symbol | DSA Use |
|---|---|---|
| AND | & | Masking, checking if bit is set: (n & (1 << i)) !== 0 |
| OR | | | Setting bits: n | (1 << i) |
| XOR | ^ | Finding unique elements, toggling bits |
| NOT | ~ | Bit inversion |
| Left shift | << | Multiply by powers of 2: 1 << n equals 2^n (up to n=30) |
| Right shift | >> | Divide by powers of 2 (preserves sign) |
| Unsigned right shift | >>> | Divide by powers of 2 (fills with 0) |
Common bit manipulation patterns:
JavaScript offers several loop forms. Use each where it fits:
Critical warning: Never use for...in on arrays. It iterates over string keys (including inherited prototype properties), not values:
JavaScript has two operators that are extremely useful for DSA and largely unique to it:
Nullish coalescing (`??`): Returns the right side only if the left is null or undefined:
Optional chaining (`?.`): Short-circuits to undefined if any part is null or undefined:
The ?? operator is safer than || for default values. map.get(key) || 0 would incorrectly replace a stored value of 0 with 0. The ?? operator only replaces null and undefined, preserving legitimate falsy values like 0, "", and false.
JavaScript offers two main ways to define functions, and both appear frequently in DSA code:
For DSA, use function declarations for your main solution functions and arrow functions for comparators, callbacks, and short helper functions. The choice is mostly style, but there is one practical difference: function declarations are hoisted, so you can call them before they appear in the code. Arrow functions are not.
JavaScript passes primitives (numbers, strings, booleans) by value and objects (arrays, maps, sets, plain objects) by reference. This is the same as Java and matters for the same reason:
This is critical for backtracking. When you add a path to your results, you must copy the array. Otherwise, all results point to the same array that gets modified during backtracking:
The [...path] spread creates a shallow copy of the array. This is the JavaScript equivalent of Java's new ArrayList<>(path).
Closures are uniquely powerful in JavaScript and relevant for memoization patterns:
You will not need closures for most standard DSA problems, but they come up in design problems (like LRU Cache) and can make memoization code cleaner.
Arrow functions and destructuring make JavaScript particularly elegant for backtracking and callback patterns. The spread operator [...path] is the idiomatic way to copy arrays in JS, and destructuring const [a, b] = pair makes working with tuples clean and readable.
Arrays are the backbone of DSA in JavaScript. They are dynamic (no fixed size), zero-indexed, and come with a rich set of built-in methods. You will use arrays in virtually every problem.
The .length property (no parentheses, just like Java arrays) gives the current size:
This is one of the most common JavaScript bugs in interviews:
The 2D array initialization gotcha is one of the most common JavaScript DSA mistakes. fill(new Array(...)) evaluates new Array(...) once and fills every slot with a reference to that single array. Use Array.from() with a mapping function to create independent row arrays. This is the kind of bug that silently produces wrong answers and wastes precious interview time debugging.
| Method | Mutates? | Returns | Time | DSA Use |
|---|---|---|---|---|
push(val) | Yes | new length | O(1) | Append to end (stack push) |
pop() | Yes | removed element | O(1) | Remove from end (stack pop) |
shift() | Yes | removed element | O(n) | Remove from front (queue dequeue) |
unshift(val) | Yes | new length | O(n) | Add to front |
splice(i, count, ...items) | Yes | removed elements | O(n) | Remove/insert at index |
slice(start, end) | No | new array | O(k) | Copy a portion |
concat(arr2) | No | new array | O(n+m) | Merge arrays |
reverse() | Yes | same array | O(n) | Reverse in place |
includes(val) | No | boolean | O(n) | Linear search |
indexOf(val) | No | index or -1 | O(n) | Find first occurrence |
fill(val, start, end) | Yes | same array | O(n) | Fill range with value |
flat(depth) | No | new array | O(n) | Flatten nested arrays |
Pay attention to the Mutates? column. Methods like sort(), reverse(), splice(), fill() modify the array in place. Methods like slice(), concat(), map(), filter() return new arrays.
Arrays are your stack in JavaScript. No need for a separate class:
Here is where JavaScript's limitations show. The naive approach uses shift() as dequeue:
For BFS on a graph with 100,000 nodes, using shift() makes the algorithm O(n^2) instead of O(n). We will cover a proper queue implementation in the Collections section.
That said, for problems with small inputs (n < 10,000), shift() is pragmatically acceptable on LeetCode. V8's internal optimizations make the constant factor small. Use your judgment based on the problem constraints.
Grid problems (number of islands, shortest path in maze) need to visit adjacent cells. This pattern appears in almost every grid BFS/DFS:
One of JavaScript's nicest features for DSA. No temporary variable needed:
Array.from() is one of the most versatile tools in JavaScript for DSA:
Strings in JavaScript are immutable, just like in Java. Every time you modify a string, JavaScript creates a new object. This has the same performance implication:
Unlike Java, JavaScript has no StringBuilder. The Array.push() + .join("") pattern is the standard alternative. Array join concatenates all elements into a single string with the separator between them.
JavaScript has no character type. A "character" is just a string of length 1:
This means you cannot compare characters with < and > in the numeric sense directly. Character comparisons work lexicographically as string comparisons, which is actually what you want for most DSA problems.
| Method | Returns | Example | DSA Use |
|---|---|---|---|
s[i] or s.charAt(i) | string | s[0] | Access character |
s.length | number | s.length | Loop bounds (property, no parens) |
s.slice(start, end) | string | s.slice(0, 3) | Substring (start inclusive, end exclusive) |
s.split(sep) | array | s.split(" ") | Tokenize |
s.indexOf(str) | number | s.indexOf("ab") | Find substring (-1 if not found) |
s.includes(str) | boolean | s.includes("ab") | Substring check |
s.startsWith(pre) | boolean | s.startsWith("ab") | Prefix check |
s.endsWith(suf) | boolean | s.endsWith("ab") | Suffix check |
s.repeat(n) | string | "ab".repeat(3) | Repeat: "ababab" |
s.trim() | string | s.trim() | Remove surrounding whitespace |
s.toLowerCase() | string | Case conversion | |
s.toUpperCase() | string | Case conversion | |
s.replace(a, b) | string | Replace first occurrence | |
s.replaceAll(a, b) | string | Replace all occurrences |
Since JavaScript has no Character utility class like Java, you use charCodeAt() and String.fromCharCode():
This is faster and more memory-efficient than Map when the character set is known and small (lowercase letters, digits, etc.).
Template literals (backtick strings) are one of JavaScript's most useful features for DSA:
The template literal ` ${row},${col} ` pattern is extremely common in JavaScript for creating composite keys in Sets and Maps. In grid BFS problems, you will use this to track visited cells. It is clean, readable, and idiomatic JavaScript.
This is the most important section of this chapter. Choosing the right collection is often the difference between an O(n) and an O(n^2) solution. JavaScript's collection landscape is simpler than Java's, but it has its own quirks.
ES6 Map is JavaScript's hash map. It offers O(1) average-case get, set, has, and delete:
Plain objects ({}) can also serve as hash maps, but with limitations:
| Feature | Map | Object |
|---|---|---|
| Key types | Any (numbers, objects, arrays) | Strings and Symbols only |
| Size | .size property | Object.keys(obj).length |
| Iteration order | Insertion order guaranteed | Mostly insertion order |
| Performance | Optimized for frequent adds/deletes | Optimized for static structures |
| Default keys | None | Has prototype properties |
| DSA recommendation | Default choice | Fine for simple string-key frequency maps |
Use Map as your default. Use objects only when you are counting frequencies of characters or short strings and want the most concise code.
Unlike Java's Set.add() which returns a boolean indicating whether the element was new, JavaScript's Set.add() returns the Set itself. To check if an element was already present, call has() before add():
Important: Sets use reference equality for objects and arrays. new Set([[1,2], [1,2]]) has size 2, not 1, because the two [1,2] arrays are different objects. For composite keys, serialize to strings: visited.add(${row},${col}).
JavaScript has no built-in queue with O(1) dequeue. Here is a lightweight implementation using an object as a sparse array with head/tail pointers:
BFS using this queue:
For level-order traversal, capture the queue size at the start of each level:
JavaScript has no built-in priority queue. For problems like top-K elements, Dijkstra's algorithm, or merge-K-sorted-lists, you need a min-heap. Here is a minimal implementation:
Usage:
In an interview, mention upfront that JavaScript lacks a built-in PriorityQueue. Most interviewers will either let you use a simplified version or accept that you know the heap interface and can implement it if needed.
JavaScript has no sorted map or sorted set. This is a genuine disadvantage for problems that need:
Workarounds:
| Collection | Type | Key Methods | Time Complexity | DSA Use |
|---|---|---|---|---|
Array | Dynamic array | push, pop, shift, splice, slice, sort | push/pop O(1), shift O(n), access O(1) | Stacks, results, DP tables |
Map | Hash map | set, get, has, delete, size | O(1) average | Frequency counting, two-sum |
Set | Hash set | add, has, delete, size | O(1) average | Visited tracking, dedup |
Object | String-key map | bracket access, in, Object.keys | O(1) average | Simple frequency maps |
Queue (custom) | FIFO queue | enqueue, dequeue, peek, size | O(1) all | BFS |
MinHeap (custom) | Priority queue | push, pop, peek, size | O(log n) push/pop | Top-K, Dijkstra |
Sorting is fundamental to DSA. And JavaScript has the single biggest sorting gotcha of any mainstream language.
Array.sort() converts elements to strings and sorts lexicographically by default. The string "10" comes before "9" because "1" < "9" in Unicode code points. Always pass a comparator for numeric sorting:
The comparator function takes two elements and returns:
a comes firstb comes firstUnlike methods like map and filter which return new arrays, sort() modifies the array in place and returns the same array reference. If you need to preserve the original:
Array.sort() is guaranteed stable in all modern JavaScript engines since ES2019. This means elements with equal sort keys maintain their original relative order. The algorithm used is TimSort (same as Java's Collections.sort() and Python's sorted()).
The (a, b) => a - b comparator can theoretically overflow if a and b are very large numbers of opposite sign. In practice, this is extremely rare in DSA because JavaScript numbers are doubles with huge range. But if you need to be safe:
JavaScript's functional array methods can make DSA code more concise. Use them when they improve readability, but do not force them when a simple loop is clearer.
JavaScript has two "nothing" values, and understanding the difference matters for DSA.
In DSA, null appears when a tree node has no left or right child, or when a linked list ends. undefined appears when you call map.get(key) for a missing key or access an array index out of bounds.
Rule 1: Guard clauses for edge cases.
Note: !null and !undefined are both true, so if (!root) catches both.
Rule 2: Use `??` for defaults, not `||`.
If the map stores 0 as a legitimate value, || 0 would incorrectly treat it as missing. The ?? operator only triggers for null and undefined.
Rule 3: Optional chaining for safe navigation.
Unlike Java, JavaScript does not throw a ConcurrentModificationException when you modify a collection during iteration. But modifying arrays during iteration still causes subtle bugs.
Unlike Java, JavaScript's Map and Set allow you to delete the current element during for...of iteration. This is part of the ES6 specification:
In DSA, you rarely need to modify collections during iteration. Most problems build new collections from existing ones. When you do need to remove elements, prefer building a new array with filter() or using the backward loop pattern.
Recursion is fundamental to DSA: tree traversals, DFS, backtracking, and divide-and-conquer all rely on it. JavaScript handles recursion the same as other languages, with one important limitation.
V8 (the engine behind Chrome and Node.js) has a call stack limit of approximately 10,000 to 15,000 frames, depending on the size of each frame. Unlike Python, there is no way to increase this limit at runtime. Unlike Java, which has a relatively generous stack size, JavaScript's limit is fixed.
| Scenario | Typical Depth | Risk |
|---|---|---|
| Balanced binary tree | O(log n) | Safe for n up to 10^6 |
| Linked list / skewed tree | O(n) | Dangerous if n > 10,000 |
| Backtracking | O(d) | Usually safe (d is decision depth) |
| DFS on graph | O(n) | Dangerous for large n |
When the input is large and recursion depth could exceed 10,000, convert to an iterative approach with an explicit stack:
JavaScript's ES6 specification includes tail call optimization (TCO), but only Safari implements it. Chrome and Node.js do not. This means even "tail-recursive" functions will still add a frame per call. Always assume recursion adds to the stack.
Graphs appear in a huge number of DSA problems: shortest path, connected components, topological sort, and more. The two main representations map directly to when each is appropriate.
When nodes are numbered 0 through n-1, use an array:
When node IDs are not contiguous (like string labels or large integers):
Store edge weights alongside destination nodes using small arrays:
| Feature | Array of Arrays | Map |
|---|---|---|
| Node IDs | Must be 0 to n-1 | Any type (int, string) |
| Memory | Compact, no hash overhead | Hash table overhead |
| Access speed | O(1) direct index | O(1) average (hash lookup) |
| Use when | Nodes are numbered, dense | Nodes are sparse, string labels |
| Most problems | This one | Graph with city names, etc. |
JavaScript has no Pair or Tuple type, but the language provides clean alternatives through arrays and destructuring.
Since arrays use reference equality ([1,2] !== [1,2]), you cannot use arrays directly as Map keys or Set values for deduplication. Serialize to strings:
Destructuring makes arrays the clear winner for tuples in JavaScript. const [a, b] = pair is cleaner and more concise than any alternative.
Two main approaches for avoiding duplicates in results.
This pattern appears in problems like 3Sum, Combination Sum II, and Subsets II. The sort brings duplicates adjacent, and the skip condition ensures each unique value is processed only once at each position.
Since JavaScript Sets compare objects and arrays by reference, you must serialize to strings for deduplication:
This is useful when you cannot sort the input or when duplicate detection is not straightforward. But JSON.stringify and JSON.parse are slower than the sort-and-skip approach, so prefer Approach 1 when possible.
Warning about `Math.max()` and `Math.min()` with arrays:
Remember: [...arr] and slice() are shallow copies. For 2D arrays, you must map over rows and spread each one individually, or the inner arrays will still be shared references.
JavaScript uses class syntax (ES6) for object-oriented code. In DSA, you need classes primarily for node definitions that LeetCode provides, and occasionally for design problems like LRU Cache.
Notice that JavaScript constructors use default parameters (val = 0, next = null) instead of constructor overloading. This is cleaner than Java's multiple constructor pattern. A single constructor handles all cases.
LeetCode provides ListNode and TreeNode definitions for you. You do not need to write them yourself, but understanding the structure is essential.
JavaScript has no private or public keywords in the traditional sense. Class fields declared with # prefix are truly private (ES2022), but this is never used in DSA. All properties are public and accessed directly:
This is the same convention as Java DSA code, where fields are package-private for conciseness.