Module F-3·14 min read

String immutability in JavaScript, why repeated concatenation kills performance, and substring/rotation patterns.

JJS
Written by Jatin Jain Saraf · Senior Software Engineer

Module F-3 — Strings: Immutability and Manipulation Patterns

What this module covers: Strings look like arrays of characters — indexable, iterable, sliceable — but JavaScript enforces one rule arrays don't have: a string can never be changed in place. This module covers what that immutability actually costs, why naive string-building in a loop is a quadratic-time trap nearly identical to the array spread mistake from Module F-2, and the substring/rotation patterns that recur across the rest of this course.


Strings Are Immutable — What That Actually Means

In JavaScript, every string operation that looks like it "modifies" a string actually creates a brand new string and leaves the original untouched.

typescript

This isn't a JavaScript quirk — it's true in Python, Java, and most modern languages. Strings are treated as values, not as mutable buffers, precisely so that two variables can safely reference "the same" string without one's changes leaking into the other. It's also what makes strings safe to use as object keys and Map/Set entries (Module F-4): a key that could silently change out from under you would break every hash-based lookup relying on it.

Analogy: A string is a printed page, not a whiteboard. If you want to change one letter, you don't erase and rewrite in place — you print a whole new page with that one letter changed, and the old page still exists exactly as it was.

Why this matters in production: immutability is a correctness guarantee, but it has a cost that's easy to miss — every "modification" you write is secretly an allocation of new memory, not a cheap in-place edit.


The Concatenation Trap

This is the string equivalent of the array-spread mistake from Module F-2, and it's one of the most common accidental-quadratic bugs in real code:

typescript

Every report += row doesn't append to report in place — it allocates an entirely new string long enough to hold the old content plus the new piece, then copies everything over. On iteration 1,000 of a 10,000-row loop, that copy is already ~1,000 characters of dead work repeated on every subsequent iteration. Summed across the whole loop, that's the same 1 + 2 + 3 + ... + n shape as the array-spread problem: O(n²) overall.

.join() avoids this because the engine can compute the total output length once, allocate a single buffer of exactly that size, and fill it — one allocation instead of thousands. The fix is almost always the same shape: accumulate pieces in an array, join once at the end, rather than concatenating a growing string on every iteration.


Reading Without Modifying: Slicing and Searching

Because strings can't be mutated, every string algorithm you write is really about reading ranges and searching — not editing.

typescript

slice() and indexOf() are both O(n) in the worst case — they may have to scan or copy across the full length of the string. That's not a problem in isolation, but it becomes one the same way array scans do: calling .indexOf() inside a loop over every character turns an O(n) scan into O(n²) overall, exactly like .includes() did for arrays in Module F-1.

Sign in to keep reading

The rest of this module is free — sign in with Google to unlock it and track your progress.

Sign in & Register

Discussion

0

Join the discussion

Loading comments...

© 2026 Jatin Jain Saraf (JJS). All rights reserved.