JavaScript Strings: Template Literals, slice vs substring
JavaScript strings are immutable UTF-16 sequences. Template literals, slice vs substring vs substr, replace vs replaceAll, and why an emoji has length 2.
- Course: JavaScript study plan
- Module: Values, types and coercion
- Kind: Lesson
- Reading time: 14 min
- Runtime: Node 16
Are strings immutable in JavaScript?
Yes. A JavaScript string is an immutable sequence of UTF-16 code units: every method that seems to change one, such as toUpperCase, replace or trim, returns a new string and leaves the original as it was, so you must assign the result — s = s.toUpperCase(). Immutability is also why strings compare by value with === and are safe to share and to use as object keys.
Lesson
Strings are the most-used values in most programs, and JavaScript's are simple in shape — immutable sequences of UTF-16 code units — with a rich, mostly consistent method set. The things that go wrong are predictable: forgetting immutability (s.toUpperCase() with no assignment), confusing slice and substring and substr, treating length as a character count when emoji are present, and reaching for the wrong search method. This lesson is the toolkit, organised by what you want to do, with the traps marked.
Literals and template literals
const a = 'single', b = "double"; // identical; pick one style (Prettier chooses double)
const t = `sum: ${1 + 2}, upper: ${a.toUpperCase()}`; // template literal: any expression inside ${}
const multi = `line one
line two`; // newlines are kept as written
const esc = "tab\tnewline\nquote\"backslash\\ unicodeé";
Template literals also allow tagged templates — ` tagtext ${x} calls tag with the literal parts and values (used by libraries for SQL, GraphQL, styled components) — a topic for the strings module later. String.raw` is one such tag that leaves escapes alone.
Immutability
Every "modifying" method returns a new string; the original never changes.
let s = "hello";
s.toUpperCase(); // returns "HELLO", s is still "hello"
s = s.toUpperCase(); // now s is "HELLO"
s[0] = "j"; // ignored in sloppy mode; a TypeError in strict mode, modules and classes
Because they are immutable, strings are safe to share, compare by value with ===, and use as object keys; and building a long string in a loop with += is fine in modern engines (they use ropes internally), unlike Java — array.join is still clearer for many pieces.
Indexing, length and Unicode
s.length counts UTF-16 code units, s[i] and s.charAt(i) read one unit, s.at(-1) reads from the end (2022). Characters outside the Basic Multilingual Plane — emoji, some CJK, mathematical symbols — take two units:
"héllo".length // 5
"😀".length // 2 — one character, two code units (a surrogate pair)
[..."😀a"].length // 2 — spreading iterates by code point
"😀".codePointAt(0) // 128512
String.fromCodePoint(128512) // "😀"
Iterate with for (const ch of s) or [...s] when characters matter; s.split("") splits by unit and corrupts emoji. normalize("NFC") before comparing text that may contain composed versus decomposed accents.
The toolkit, by task
Search
s.includes("lo") // boolean
s.indexOf("l"), s.lastIndexOf("l") // -1 when absent
s.startsWith("he"), s.endsWith("lo")
s.search(/l+/) // regex, index or -1
s.match(/l/g) // ["l", "l"] or null; s.matchAll(/l/g) for iterator with groups
Slice
s.slice(1, 3) // "el" — end exclusive; negatives count from the end: s.slice(-3) === "llo"
s.substring(1, 3) // "el" — swaps arguments if start > end; negatives become 0 — prefer slice
s.substr(1, 2) // deprecated (start, length) — do not use
Transform
s.toUpperCase(), s.toLowerCase() // toLocaleUpperCase for locale rules
s.trim(), s.trimStart(), s.trimEnd()
s.padStart(8, "0"), s.padEnd(8) // "000hello"
s.repeat(3)
s.replace("l", "L") // FIRST occurrence only when given a string
s.replaceAll("l", "L") // every occurrence (2021); or s.replace(/l/g, "L")
s.split(","), s.split(/\s+/), s.split("", 3)
"a,b".concat("!") // same as + ; rarely used
The replace-with-a-string-replaces-once rule is the trap people hit weekly; replaceAll or a /g regex is the fix. In the replacement string $& is the match and $1 a group; a function replacer (match, g1) => … computes the replacement.
Compare
"a" < "b" // true — code unit order; "Z" < "a" is true, "10" < "9" is true
"ä".localeCompare("z", "de") // -1: locale-aware order for sorting user-visible text
arr.sort((x, y) => x.localeCompare(y))
Default </> on strings compares code units, so upper case sorts before lower case and digit strings compare lexically. For human-facing sort use localeCompare, optionally with { numeric: true } to make "file10" follow "file9".
Conversions
String(x) handles every value (String(null) is "null", String([1,2]) is "1,2", String({}) is "[object Object]"); x.toString() fails on null/undefined; ` ${x} is String(x); JSON.stringify(x) gives a JSON encoding (quotes around strings, "[1,2]" for arrays). Number to string with a radix: (255).toString(16) is "ff"`. String to number: previous lesson.
Building strings
For a few pieces, template literals. For many pieces in a loop, either += (fine in V8) or push to an array and join — the join form makes the separator explicit and avoids the trailing-separator problem. Array(n).fill("-").join("") or "-".repeat(n) for runs.
Interview angle
- "Are strings mutable?" No — every method returns a new string; assign the result.
- "
sliceversussubstringversussubstr?"slicesupports negatives and is the one to use;substringswaps and clamps;substris deprecated. - "Why is
\"😀\".length2?" UTF-16 code units; a surrogate pair. Iterate withfor…ofor spread for code points. - "How do you replace all occurrences?"
replaceAllorreplacewith a/gregex — plainreplace(string)does one. - "Why does
[\"b\", \"a\", \"B\"].sort()putBfirst?" Code-unit order; uselocaleComparefor human sorting.
Key takeaways
- Immutable UTF-16 sequences; template literals interpolate any expression.
length/indexing count code units; emoji are two;for…of/spread iterate characters.slice(negatives ok), neversubstr;includes/startsWith/indexOfto search;replaceAllfor every match.<compares code units;localeComparefor people;{ numeric: true }forfile10.String(x)converts anything;joinan array for many pieces.
Common questions
What is the difference between slice and substring in JavaScript?
Both return part of a string from a start index up to an exclusive end. slice accepts negative indexes that count from the end, so s.slice(-3) is the last three units, while substring turns negatives into 0 and swaps its arguments when start is greater than end. Prefer slice; substr is deprecated.
How do you replace all occurrences of a string in JavaScript?
Use s.replaceAll("a", "b") (ES2021) or replace with a regular expression that has the g flag. Plain replace with a string pattern replaces only the first occurrence, which is the trap people hit most often.
Why is the length of an emoji 2 in JavaScript?
length counts UTF-16 code units, not characters, and characters outside the Basic Multilingual Plane, such as most emoji, are stored as a surrogate pair of two units. Iterate with for…of or spread [...s] to work by code point; split("") breaks emoji apart.
How do you sort strings alphabetically in JavaScript?
Use arr.sort((x, y) => x.localeCompare(y)). The default sort and the < operator compare UTF-16 code units, so upper case sorts before lower case and "10" comes before "9"; localeCompare with { numeric: true } also puts "file10" after "file9".
What are template literals in JavaScript?
Template literals are strings written in backticks that interpolate any expression with ${…} and may span several lines, keeping the newlines as written. A tag function placed before one makes a tagged template, which libraries use for SQL, GraphQL and styled components.
Exercises
Code units and characters
Read one line. Print length=<s.length> codePoints=<[...s].length>, then reversed=<the characters reversed> — reverse the spread array so surrogate pairs stay intact — then upper=<s.toUpperCase()> and first=<first character> last=<last character> (empty for an empty line).
Example: héllo 😀 →
length=8 codePoints=7
reversed=😀 olléh
upper=HÉLLO 😀
first=h last=😀Replace, count, pad
Read lines until end of input (skip empty lines). For each line s print four lines: once=<s.replace("a", "_")>, all=<s.replaceAll("a", "_")>, words=<number of whitespace-separated words after trimming> and padded=<s.trim().padStart(12, ".")>.
Example: banana →
once=b_nana
all=b_n_n_
words=1
padded=......bananaIn this module: Values, types and coercion
- The seven primitives, objects and typeof
- Numbers — doubles, NaN, safe integers and BigInt
- Strings — immutable text, template literals and the method toolkit (this lesson)
- Truthiness, coercion and the two equalities
- let, const and var — scope, hoisting and the temporal dead zone
- Checkpoint — Values, types and coercion
← Numbers — doubles, NaN, safe integers and BigInt · Truthiness, coercion and the two equalities →