Module F-3·12 min read

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

Written by Jatin Jain Saraf · Architect of SupraScan

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.

let name = 'jatin';
name[0] = 'J';        // silently does nothing — strings can't be mutated by index
console.log(name);    // still "jatin"
 
let upper = name.toUpperCase(); // creates and returns a NEW string
console.log(name);     // "jatin" — unchanged
console.log(upper);    // "JATIN" — a different string entirely

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:

// O(n²) in the worst case — treat every += as if it copies everything built so far
function buildReportSlow(rows: string[]): string {
  let report = '';
  for (const row of rows) {
    report += row + '\n'; // allocates a new string the length of (report so far + row)
  }
  return report;
}
 
// O(n) — join allocates once, at the end, computing the total length up front
function buildReportFast(rows: string[]): string {
  return rows.join('\n');
}

Every report += row doesn't append to report in place — the naive mental model (and the literal behavior in many language runtimes) is that 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.

Engine note: V8 actually defers some of this cost by representing a += b as a lightweight "rope" internally, rather than eagerly copying on every single +=. But that copy still has to happen once the string is read — iterated character-by-character, regex-matched, or passed somewhere that needs a flat buffer — so the O(n²) risk doesn't disappear, it just moves to wherever the string is next consumed. Treating every += as a full copy is still the safest mental model.

.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.

const s = 'the quick brown fox';
 
s.slice(4, 9);      // "quick" — a substring from index 4 up to (not including) 9
s.indexOf('brown');  // 10 — the starting index of the first match, or -1 if absent
s.split(' ');        // ["the", "quick", "brown", "fox"] — an array, safe to mutate freely

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...