Arrays, Strings and Hash Maps in JavaScript
These three structures cover the majority of day-to-day code and interview questions. The key skill is knowing what each built-in method costs — the pleasant syntax hides very different price tags.
Arrays
JavaScript arrays are ordered, indexable, and dynamic. What each common operation costs:
| Operation | Complexity | Why |
|---|---|---|
arr[i] read/write | O(1) | Direct index access |
push / pop | O(1) amortized | Works at the end, nothing shifts |
shift / unshift | O(n) | Every remaining element is re-indexed |
includes / indexOf / find | O(n) | Linear scan |
slice | O(k) | Copies k elements |
splice | O(n) | Shifts everything after the edit point |
concat / spread [...a, ...b] | O(n + m) | Copies both inputs |
map / filter / reduce | O(n) | One full pass plus the callback cost |
sort | O(n log n) | Comparison sort |
The two traps worth internalizing:
Trap 1 — shift in a loop. Draining an array from the front re-indexes the whole array every time, making the loop O(n^2):
const tasks = ["deploy", "review", "standup", "triage"];
while (tasks.length > 0) {
const task = tasks.shift(); // O(n) each time
console.log(task);
}
For hot paths, read with an index instead — same order, O(n) total:
const queue = ["deploy", "review", "standup", "triage"];
let head = 0;
while (head < queue.length) {
const task = queue[head++];
console.log(task);
}
Trap 2 — includes inside a loop. A linear scan inside a linear loop is O(n^2). See the Set section below for the O(n) fix.
Strings
Strings are immutable: every "modification" creates a new string.
| Operation | Complexity | Notes |
|---|---|---|
s[i] / charAt(i) / length | O(1) | |
s + t concatenation | O(n + m) | Allocates a new string |
slice / substring | O(k) | |
includes / indexOf | O(n) | Substring scan |
split / join | O(n) | The standard bridge to array tools |
Because strings are immutable, building one character by character with += in a loop can degrade badly. The idiomatic pattern is to collect pieces in an array and join once:
function initials(names) {
const parts = [];
for (const name of names) {
parts.push(name[0]);
}
return parts.join(".");
}
console.log(initials(["Rizwan", "Ashiq"])); // "R.A"
And the classic reverse, using the split–reverse–join bridge:
function reverse(s) {
return s.split("").reverse().join("");
}
console.log(reverse("Hafsa")); // "asfaH"
Hash maps: Map, Set, and plain objects
Hash-based structures are the workhorses of fast code: get, set, has, and delete are all O(1) on average.
Map vs plain object
| Aspect | Map | Plain object |
|---|---|---|
| Key types | Anything, including objects | Strings and symbols only — numbers coerce |
| Size | map.size | Object.keys(obj).length, an O(n) call |
| Iteration | Insertion order, directly iterable | Needs Object.keys / entries |
| Prototype baggage | None | Inherits keys like toString |
Use a Map when keys are dynamic data (user IDs, words, coordinates). Plain objects are fine as fixed-shape records.
Counting with a Map
The frequency-counter shape — count in one pass, answer in the next — solves an entire family of problems:
function mostFrequent(names) {
const counts = new Map();
for (const name of names) {
counts.set(name, (counts.get(name) || 0) + 1);
}
let best = null;
let bestCount = 0;
for (const [name, count] of counts) {
if (count > bestCount) {
best = name;
bestCount = count;
}
}
return best;
}
const attendees = ["Ayesha", "Ibrahim", "Ayesha", "Zakariya", "Ayesha", "Ibrahim"];
console.log(mostFrequent(attendees)); // "Ayesha"
O(n) time, O(k) space for k distinct keys.
Membership with a Set
The single most useful refactor in JavaScript performance work — replacing a scan-in-a-loop with a Set:
const registered = ["Rizwan", "Hafsa", "Ibrahim", "Ayesha"];
const checkedIn = ["Hafsa", "Zakariya", "Rizwan"];
// O(n * m): registered.includes runs a full scan per check-in
const slowMatches = checkedIn.filter((name) => registered.includes(name));
// O(n + m): build the Set once, each lookup is O(1)
const registeredSet = new Set(registered);
const fastMatches = checkedIn.filter((name) => registeredSet.has(name));
console.log(fastMatches); // ["Hafsa", "Rizwan"]
At 10,000 names each, that is the difference between 100 million comparisons and about 20,000 operations.
Deduplication is a one-liner for the same reason:
const visits = ["Rizwan", "Hafsa", "Rizwan", "Ayesha", "Hafsa"];
const uniqueVisitors = [...new Set(visits)];
console.log(uniqueVisitors); // ["Rizwan", "Hafsa", "Ayesha"]
Choosing between them
- Need order and index access → array.
- Need fast lookup, counting, or "have I seen this" → Map / Set.
- Text processing → string methods, bridging to arrays via
split/joinwhen you need array tools. - Repeatedly inserting or removing at the front → none of the above; see Stacks, Queues and Linked Lists.
Practice problems for this page: the Array and String sections of the practice catalog.