Can you write this from memory?
Check if string `item_text` matches the pattern `/abc/`.
slice or substring? In JavaScript, slice supports negative indices; substring clamps negatives to 0.
replace or replaceAll? replace only hits the first match unless you use a regex with /g.
Why is '😄'.length === 2? JavaScript strings are UTF-16, not Unicode graphemes.
JavaScript string methods look similar but behave differently. We drill the distinctions that cause real bugs: index handling, replacement patterns, and Unicode surprises.
Quick reference:
slice(-2)→ last 2 characterssubstring(-2)→ same assubstring(0)(negatives clamped)replace("x", "y")→ first match onlyreplaceAll("x", "y")→ all matchesreplaceAll(/x/, "y")→ TypeError (needs/gflag)'👍'.length→ 2 (UTF-16 code units, not characters)
Both extract substrings, but they handle edge cases differently:
const str = "JavaScript";
// Basic usage (identical)
str.slice(0, 4) // "Java"
str.substring(0, 4) // "Java"
// Negative indices (different!)
str.slice(-6) // "Script" (counts from end)
str.substring(-6) // "JavaScript" (negatives → 0)
// Reversed arguments
str.slice(4, 0) // "" (empty, no swapping)
str.substring(4, 0) // "Java" (swaps to 0, 4)
Rule of thumb: Use slice(). It's more predictable and matches array behavior. For a printable reference of all string methods, see the JavaScript string methods cheat sheet.
| Method | Negative indices | Swaps args if start > end |
|---|---|---|
slice() | Supported (from end) | No (returns empty) |
substring() | Treated as 0 | Yes |
// You expect all underscores replaced...
"a_b_c".replace("_", "-") // "a-b_c" (only first!)
// replaceAll fixes this
"a_b_c".replaceAll("_", "-") // "a-b-c"
The regex /g requirement
When using regex with replaceAll(), you MUST include the global flag:
"a1a2".replaceAll(/a/, "b") // TypeError!
"a1a2".replaceAll(/a/g, "b") // "b1b2"
This is intentional. It prevents the confusing case where replaceAll with a regex would only replace the first match.
When to use which
- String pattern, replace all:
replaceAll("old", "new") - String pattern, replace first:
replace("old", "new") - Regex pattern, replace all:
replace(/pattern/g, "new")orreplaceAll(/pattern/g, "new") - Regex pattern, replace first:
replace(/pattern/, "new")
Template literals (backticks) support three features:
1. Interpolation
const name = "Alice";
const greeting = `Hello, ${name}!`; // "Hello, Alice!"
// Expressions work too
`Total: $${(price * 1.1).toFixed(2)}`
2. Multi-line strings
const html = `
<div>
<h1>${title}</h1>
<p>${content}</p>
</div>
`;
3. Tagged templates
A tag function receives the template parts separately:
function highlight(strings, ...values) {
return strings.reduce((acc, str, i) => {
const value = values[i] !== undefined ? `<mark>${values[i]}</mark>` : "";
return acc + str + value;
}, "");
}
const name = "Alice";
highlight`Hello, ${name}!`
// "Hello, <mark>Alice</mark>!"
Critical misconception: Tagged templates don't automatically sanitize or escape anything. The tag function must implement that logic. A call like html\
is NOT safe unless yourhtml` tag escapes HTML entities.
JavaScript strings are sequences of UTF-16 code units, not Unicode characters:
"hello".length // 5, as expected
"👍".length // 2, not 1!
"👍🏽".length // 4 (emoji + skin tone modifier)
"café".length // 4 or 5 depending on encoding
Why this happens
UTF-16 represents characters outside the Basic Multilingual Plane (BMP) as "surrogate pairs": two 16-bit code units. Many emoji fall outside the BMP.
Counting actual characters (graphemes)
Use Intl.Segmenter for accurate character counts:
function graphemeLength(str) {
const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
return [...segmenter.segment(str)].length;
}
graphemeLength("hello") // 5
graphemeLength("👍") // 1
graphemeLength("👍🏽") // 1 (emoji + modifier = 1 grapheme)
When this matters
- Character limits in UI (Twitter, form fields)
- Text truncation with ellipsis
- Cursor positioning in text editors
Comparing strings with accents
// Wrong: byte comparison
["ä", "z", "a"].sort() // ["a", "z", "ä"] (ä after z!)
// Right: locale-aware
["ä", "z", "a"].sort((a, b) => a.localeCompare(b, "de"))
// ["a", "ä", "z"] (German order)
For repeated comparisons, use Intl.Collator
const collator = new Intl.Collator("de");
const sorted = names.sort(collator.compare); // Much faster for large arrays
normalize() for string equality
The same character can have multiple Unicode representations:
const a = "café"; // 'é' as single codepoint (U+00E9)
const b = "cafe\u0301"; // 'e' + combining acute accent
a === b // false!
a.normalize() === b.normalize() // true
Always normalize() user input before comparing or storing if accented characters are possible. If you're coming from Python, the Python vs JavaScript variables comparison covers how string handling differs between the two languages.
slugify (URL-safe string)
function slugify(str) {
return str
.toLowerCase()
.normalize("NFD") // Decompose accents
.replace(/[\u0300-\u036f]/g, "") // Remove accent marks
.replace(/[^a-z0-9]+/g, "-") // Non-alphanumeric → dash
.replace(/^-|-$/g, ""); // Trim leading/trailing dashes
}
slugify("Héllo Wörld!") // "hello-world"
camelCase to kebab-case
These recipes rely on regex patterns. For a copy-paste reference of common regex syntax, see the JavaScript regex cheat sheet.
function toKebab(str) {
return str
.replace(/([a-z])([A-Z])/g, "$1-$2") // Insert dash between lower-upper
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2") // Handle acronyms
.toLowerCase();
}
toKebab("helloWorld") // "hello-world"
toKebab("XMLHttpRequest") // "xml-http-request"
Safe HTML escaping
function escapeHtml(str) {
const entities = {
"&": "&",
"<": "<",
">": ">",
'"': """,
"'": "'"
};
return str.replace(/[&<>"']/g, c => entities[c]);
}
Truncate with ellipsis (grapheme-safe)
function truncate(str, maxLength) {
const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
const graphemes = [...segmenter.segment(str)];
if (graphemes.length <= maxLength) return str;
return graphemes.slice(0, maxLength - 1).map(g => g.segment).join("") + "…";
}
truncate("Hello 👍🏽!", 8) // "Hello 👍🏽!" (fits)
truncate("Hello 👍🏽!", 7) // "Hello …" (truncated correctly)
| Task | Method |
|---|---|
| Extract from end | slice(-n) |
| Replace first match | replace(str, new) |
| Replace all matches | replaceAll(str, new) |
| Check contains | includes(str) |
| Check start/end | startsWith(), endsWith() |
| Trim whitespace | trim(), trimStart(), trimEnd() |
| Pad to length | padStart(n), padEnd(n) |
| Get last char | at(-1) |
| Split to array | split(delimiter) |
| Join array | arr.join(delimiter) |
| Locale sort | localeCompare() |
| Character count | Intl.Segmenter |
When to Use JavaScript String Methods
- Format user-facing messages with template literals.
- Parse and clean text input with split/trim/replace.
- Use replaceAll (or regex with /g) for structured transformations.
- Handle Unicode safely when character count matters (emoji, accents).
- Use tagged templates for DSLs (HTML escaping, SQL, CSS-in-JS).
Check Your Understanding: JavaScript String Methods
Convert "helloWorld" to "hello-world". Handle "XMLHttpRequest" sensibly.
Insert dashes at case boundaries, then lowercase. A naive regex `str.replace(/([A-Z])/g, "-$1").toLowerCase()` produces `x-m-l-http-request`. For proper acronym handling, use two passes: `str.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2").toLowerCase()` → `xml-http-request`.
What You'll Practice: JavaScript String Methods
Common JavaScript String Methods Pitfalls
- Strings are immutable (methods return new strings, not modifications)
- Confusing slice() vs substring() (use slice for predictability)
- replace() only replaces first match for string patterns
- replaceAll() with regex requires /g flag or throws TypeError
- str.length counts UTF-16 code units, not characters (emoji surprise)
- Tagged templates don't automatically escape/sanitize (the tag function must do that)
- Comparing strings with accents without normalize() or localeCompare()
- Using indexOf() !== -1 when includes() is clearer
JavaScript String Methods FAQ
slice() or substring()?
`slice()` supports negative indices (counting from the end); `substring()` clamps negatives to 0 and may swap arguments if start > end. Prefer `slice()`. It's more predictable and matches array behavior.
replace() or replaceAll()?
`replace()` only replaces the first match when given a string pattern. `replaceAll()` replaces all matches. If you use `replace()` with a regex, add the `/g` flag to replace all.
Why does replaceAll(/pattern/) throw a TypeError?
When you pass a RegExp to `replaceAll()`, it must have the global (`/g`) flag. Without it, JavaScript throws a TypeError. This prevents accidentally replacing only the first match when you expected all.
Are strings mutable in JavaScript?
No. All string methods return new strings. They never modify the original. You need to reassign: `str = str.toUpperCase()`, not just `str.toUpperCase()`.
Why is str.length wrong for emoji?
`length` returns the number of UTF-16 code units, not user-perceived characters. Many emoji (like 👍🏽) use multiple code units. `"👍".length` is 2, `"👍🏽".length` is 4.
How do I count user-perceived characters (graphemes)?
Use `Intl.Segmenter` with `granularity: "grapheme"`: `[...new Intl.Segmenter("en", { granularity: "grapheme" }).segment(str)].length`. This correctly counts emoji, accented characters, and other multi-codepoint sequences as single characters.
How do I compare/sort strings with accents correctly?
Use `localeCompare()` for comparisons or `Intl.Collator` for repeated sorting. These handle locale-specific rules: `"ä".localeCompare("z", "de")` returns negative (ä before z in German), positive in Swedish.
Why do two strings that look identical fail equality checks?
Unicode can represent the same glyph multiple ways (composed vs decomposed). Use `str.normalize()` to canonicalize before comparing: `"café".normalize() === "cafe\u0301".normalize()`.
What are tagged template literals for?
Tagged templates let you process template strings with a function. The tag receives an array of string literals and the interpolated values separately. Use cases: HTML escaping, SQL parameterization, CSS-in-JS, i18n. The tag does NOT automatically sanitize. You must write that logic.
How do I safely build HTML with template literals?
Template literals don't escape HTML by default. Either use a tagged template that escapes (like lit-html), or escape manually: `str.replace(/[&<>"']/g, c => ({"&":"&","<":"<",">":">",""":""","'":"'"})[c])`.
How do I check if a string contains a substring?
Use `includes()`: `"hello".includes("ell")` returns `true`. For the position, use `indexOf()` which returns -1 if not found. Prefer `includes()` for existence checks. It's clearer than `indexOf() !== -1`.
How do I check if a string starts or ends with a value?
Use `startsWith()` and `endsWith()`. Both accept an optional position parameter: `"hello".startsWith("ell", 1)` returns true.
How do I get the last character of a string?
Use `str.at(-1)` (ES2022). This is cleaner than `str[str.length - 1]` or `str.charAt(str.length - 1)`. The `at()` method supports negative indices just like `slice()`.
How do I write multiline strings?
Use template literals (backticks): `` `line1\nline2` `` preserves the actual newlines. For older code, you'd concatenate strings or use `\n`. Template literals also allow embedded expressions: `` `Hello ${name}` ``.
JavaScript String Methods Syntax Quick Reference
const name = "Alice";
const greeting = `Hello, ${name}!`;"abcd".slice(-2) // "cd" (last 2)
"abcd".substring(-2) // "abcd" (negatives → 0)"a_a_a".replace("_", "-") // "a-a_a" (first only)
"a_a_a".replaceAll("_", "-") // "a-a-a" (all)"a1a2".replaceAll(/a/, "b") // TypeError!
"a1a2".replaceAll(/a/g, "b") // "b1b2""hello".length // 5
"👍".length // 2 (UTF-16 code units)
"👍🏽".length // 4 (emoji + skin tone)const seg = new Intl.Segmenter("en", { granularity: "grapheme" });
[...seg.segment("👍🏽")].length; // 1function html(strings, ...values) {
return strings.reduce((acc, str, i) =>
acc + str + (values[i] ? escape(values[i]) : ""), "");
}
const input = "<script>alert(1)</script>";
const safe = html`<div>${input}</div>`;"ä".localeCompare("z", "de") // negative (ä < z in German)
"ä".localeCompare("z", "sv") // positive (ä > z in Swedish)const a = "café"; // composed
const b = "cafe\u0301"; // decomposed
a === b // false
a.normalize() === b.normalize() // trueconst text = " Hello World ";
const clean = text.trim().toLowerCase();
const sentence = "one two three";
const words = sentence.split(" ").filter(Boolean);"42".padStart(5, "0") // "00042"
"hi".padEnd(10, ".") // "hi........""hello".at(-1) // "o" (last char)
"hello".at(-2) // "l"JavaScript String Methods Sample Exercises
Get the length of string `item_name`.
item_name.lengthCreate a string with "Line 1" and "Line 2" on separate lines using backticks.
`Line 1
Line 2`Create a string "Total: ..." with the result of `price * quantity`.
`Total: ${price * quantity}`+ 14 more exercises