Building strings piece by piece
Make a new string by collecting small pieces in an array and joining them once at the end.
O(n)Space O(n)7 practice problemsStep-through pictureThe idea
In JavaScript a string can't be changed after it is made. Writing name[0] = "X" does nothing. Every change, like adding a letter, makes a brand new string.
So when you transform text, such as turning marginTop into margin-top, you read the old string one character at a time and build the new one. A simple, predictable way is to push each piece into an array called parts and call parts.join("") once at the end.
This pattern shows up whenever you clean, format, escape, or convert text: slugs for URLs, CSS property names, masked emails, or wrapped lines in a text box.
An everyday example
Writing a sign with letter tiles
You want to copy the sign marginTop but in a new style. You don't scratch the old sign. You walk along it letter by letter. For each letter you pick a new tile and place it on a tray. When you reach a capital letter, you first put a dash tile on the tray, then the small version of the letter. At the end you glue the whole tray into one new sign.
| In the example | In the code |
|---|---|
| The old sign you read from | The input string name |
| Your finger on the current letter | The index i and char = name[i] |
| The tray of tiles | The array parts |
| Placing a dash before a capital | parts.push("-") |
| Gluing the tray into one sign | parts.join("") |
From the slow way to building strings piece by piece
- Say you want to turn
fontSizeintofont-sizeby editing. Find a capital, then rebuild the string around it:result.slice(0, k) + "-" + lower + result.slice(k + 1). Each edit copies the whole string, so a name withccapitals costsO(n · c), which reachesO(n^2)when most letters are capitals. - Notice that every character of the output comes from reading the input once, from left to right. The output is the input with small local changes: a letter made lowercase here, a
-added there. So instead of editing a finished string again and again, read each input character once and write down what it becomes. - Each input character becomes one or two output pieces: its lowercase form, with a
-in front if it starts a new word. Collect the pieces in order in an arrayparts. A push costsO(1), and nothing already pushed is ever copied again. - At the end,
parts.join("")copies every piece into the final string exactly once. Read once, decide per character, push the pieces, join once: that loop is building strings piece by piece.
How it works
- Make an empty array
parts. It will hold the pieces of the answer in order. - Walk the input with an index
ifrom0to the last character, readingchar = name[i]. - Decide what this character turns into. Here
lower = char.toLowerCase()is the small version. - If the character needs extra output first, push that: a capital letter that isn't the first character gets a
"-"in front. - Push the piece for the character itself,
lower. - After the loop, return
parts.join(""), which glues every piece into one string.
Watch it run
This picture runs the code below on a real input. Press Step to move one step at a time, or Play to watch. The highlighted line is the one that just ran. Change the input to see other cases.
See every step at onceThe same run, laid out side by side
The code
Point at a note to light up the lines it explains.
function toKebabCase(name) {const parts = [];for (let i = 0; i < name.length; i++) {const char = name[i];const lower = char.toLowerCase();if (char !== lower && i > 0) {parts.push("-");}parts.push(lower);}return parts.join("");}
How fast it is
| Time | O(n) |
|---|---|
| Extra memory | O(n) |
Each of the n characters is read once and adds at most two pieces. The pieces and the result take memory that grows with n.
- Best case
O(n)Any name, even one with no capitals like
color: every character is read once and pushed once. - Average case
O(n)A typical name like
borderTopWidth: one push per letter, plus one extra push per capital. - Worst case
O(n)Every character after the first is a capital, like
aBCDE: two pushes each, sopartsholds2n - 1pieces, still linear. It returnsa-b-c-d-e.
Common mistakes
- Trying to change a character in place with
text[i] = "x". Strings can't be changed, so nothing happens. Build a new string instead. - Forgetting the first character. Without the
i > 0check,Buttonbecomes-button. - Calling
join()with no argument. It uses a comma, so you getm,a,r,g,i,n. Pass""to glue pieces with nothing between them. - Using
split("")on text with emoji or other characters outside the basic range. They are made of two code units and get split in half. UseArray.from(text)when that matters.
When to use it
Look for these clues in a problem:
- The task returns a new string made from an old one, such as a converted, cleaned, or escaped version.
- Each output character depends on one input character and maybe its neighbors.
- You need to insert, drop, or repeat characters while walking the text.
- The answer is text made of many small pieces, like words joined by commas or lines joined by newlines.
Check yourself
Answer each one in your head first, then open it.
Why doesn't name[0] = "X" change name?
JavaScript strings can't be changed after they're made. The assignment is ignored, or throws a TypeError in strict mode, so you have to build a new string.
What does toKebabCase("aBC") return, and how many pieces does parts hold before the join?
It returns a-b-c. parts holds 5 pieces: a, -, b, -, and c.
Why is char !== lower a good test for a capital letter?
Only letters that have a lowercase form change under toLowerCase(). Digits, symbols, and lowercase letters stay the same, so they never get a - in front.
What would parts.join() with no argument return for SvgIcon?
s,v,g,-,i,c,o,n, because join puts a comma between pieces when no separator is given.
Practice problems
Stuck on one? Tuto is in the playground too, and gives you hints, never the answer.
0 of 7 solved. Solve one to earn the bronze medal, half for silver, and all of them for gold.
Start practicingeasy
Go further
You don't need these to start practicing. Open one when you want more.
Why it works
The reasoning that makes it always right
The rule the loop keeps: after handling indexes 0 to i, parts.join("") is exactly the kebab-case form of name.slice(0, i + 1). Before the loop, parts is empty and so is the prefix, so the rule holds. Each step only appends what character i turns into, so the rule stays true.
Each character's output depends only on the character itself and on whether i > 0: lower it, and put a - first if it was a capital after the start. Nothing that comes later can change what an earlier character should become, so pieces never need to be revisited or edited.
The loop ends after name.length steps, and then the rule covers the whole name. The join reads each piece once, so the total work is proportional to the length of the output, which is at most 2n - 1 characters for n input characters.
Going deeper
3 more ideas
Why a capital letter tells you where a word starts
A name like backgroundColor is written in camelCase: every word after the first starts with a capital letter. A character is a capital when it changes under toLowerCase(), so char !== lower is the test. The first character is skipped (i > 0) so a name like Button becomes button, not -button.
How fast it is
The loop looks at each of the n characters once and pushes at most two pieces for each, so the work is O(n). join walks all pieces once more, which is also O(n). The array of pieces and the result use O(n) extra memory.
Is += slow in JavaScript?
In some languages, adding to a string in a loop copies the whole string each time, which costs O(n^2). Modern JavaScript engines avoid most of that cost for result += piece, so both styles are usually fine. The array of parts is still handy when you need to change a piece later, reverse the order, or join with a separator like ", ".
Work it by hand
Every step in a table, with why
Turn a camelCase name that starts with a capital into kebab-case.
name = "SvgIcon"
| Step | i | char | lower | char !== lower && i > 0 | Pushed | What happens | Why |
|---|---|---|---|---|---|---|---|
| 1 | 0 | S | s | false (i is 0) | "s" | parts spells s | S is a capital, but it's the first character, so no - goes in front. Only s is pushed. |
| 2 | 1 | v | v | false | "v" | parts spells sv | A lowercase letter doesn't change under toLowerCase(), so it's pushed as it is. |
| 3 | 2 | g | g | false | "g" | parts spells svg | Same again. |
| 4 | 3 | I | i | true | "-", "i" | parts spells svg-i | A capital after the start: a new word begins, so - is pushed first, then i. |
| 5 | 4 | c | c | false | "c" | parts spells svg-ic | Back to plain lowercase letters. |
| 6 | 5 | o | o | false | "o" | parts spells svg-ico | One piece pushed. |
| 7 | 6 | n | n | false | "n" | parts spells svg-icon | The last character. parts now holds 8 pieces for 7 input characters. |
| 8 | - | - | - | - | - | parts.join("") | The join copies the 8 pieces into one new string, once. |
It returns svg-icon. The loop read each of the 7 characters once and pushed 8 pieces; the only copy of the whole result happened in the final join.
Compared with other ways
When to pick something else
| Approach | Time | Extra memory | Choose it when |
|---|---|---|---|
| Building strings piece by piece | O(n) | O(n) | Any change that reads text once and writes new text, especially with special cases per character. |
result += piece in a loop | O(n) | O(n) | Plain appends in JavaScript. In languages without this engine help, like Java with plain strings, it can become O(n^2); use a builder there. |
One replace with a regular expression | O(n) | O(n) | The rule fits a pattern, like name.replace(/[A-Z]/g, (c) => "-" + c.toLowerCase()). It's short, but it puts a - before a capital first letter unless you handle that, and special cases get hard to add. |
Edit with slice at each change | O(n^2) | O(n) | Only for one or two edits in a long string, since each edit copies the whole string. |
Extra memory: O(n): parts holds up to 2n - 1 pieces, and the result has up to 2n - 1 characters.
How the steps grow
- Building strings piece by piece,
result += piecein a loop, Onereplacewith a regular expressionO(n) - Edit with
sliceat each changeO(n^2)
| Approach | n = 10 | n = 1,000 | n = 1,000,000 |
|---|---|---|---|
| Building strings piece by piece | 10 | 1,000 | 1 million |
result += piece in a loop | 10 | 1,000 | 1 million |
One replace with a regular expression | 10 | 1,000 | 1 million |
Edit with slice at each change | 100 | 1 million | 1 trillion |
Inputs that need care
5 tricky inputs
- An empty name:
"" - The loop never runs and
[].join("")returns"". - A capital first letter:
"SvgIcon" i > 0is false at index 0, so no-goes in front, and the result issvg-icon.- Capitals in a row:
"zIndexUI" - Every capital gets its own
-, so it returnsz-index-u-i. IfUIshould stay one word, add the-only when the character before is lowercase, or the one after is. - Digits:
"a1B" "1".toLowerCase()is"1", so a digit never counts as a capital. It returnsa1-b.- Accented capitals:
"farbeÄndern" "Ä".toLowerCase()is"ä", so the capital test works beyond English letters, and it returnsfarbe-ändern.
Variations and follow-ups
Twists interviewers add
The reverse: kebab-case to camelCase
Walk margin-top. When you see -, push nothing and set a flag; push the next character in uppercase and clear the flag. The same parts array works, with a flag instead of a look at the previous character.
Escaping text for HTML
To show user text safely as HTML, walk the characters and push < for <, > for >, & for &, and the character itself otherwise. One character can become several, and the array keeps that simple.
Joining with a separator
When the pieces are words, push the words and call parts.join(" ") or parts.join(", "). join only puts the separator between pieces, so there's no extra separator to trim at the end.
Using += instead
result += piece in a loop is also linear in modern JavaScript engines, which keep the pieces as a tree and copy them into one string only when it's needed. Keep parts when you need to change a piece later, reverse the order, or join with a separator.
In an interview
Spot it, explain it, answer follow-ups
- Spot it whenever the output is the input with local changes: change case, escape characters, compress, mask, or insert separators. If each piece of the output depends only on nearby input, one pass with an array of pieces fits.
- Say out loud that JavaScript strings can't be changed, so you'll build a new one, and that pushing pieces then joining once keeps the work linear.
- Test by hand: an empty string, a capital first letter, and two capitals in a row. Mention what you'd do for acronyms like
UIif the interviewer cares. - A common follow-up is "isn't
+=slow?". In modern JavaScript engines it's usually fine, because they join pieces lazily. In Java or C# you'd use aStringBuilder, which is the same idea asparts.
Where frontend code uses it
The same idea in real UI code
- React turns style object keys like
backgroundColorinto CSS property names likebackground-colorwhen it renders styles on the server. - Building a
classNamefrom conditions, such as["btn", isActive && "btn-active"].filter(Boolean).join(" "), is this pattern; small libraries likeclsxdo the same. - Turning a blog post title into a URL slug, or escaping
<and&before inserting user text into HTML, walks the text and builds a new string.