Strings
Mental model: A string is immutable, so every string method hands you a new string and leaves yours alone.
Level: beginner · about 13 minutes
let title = 'javascript';
console.log(title.toUpperCase()); // 'JAVASCRIPT'
console.log(title); // 'javascript', untouched
title = title.replace('j', 'J'); // you have to reassign
console.log(title); // 'Javascript'Every method returns a new string. The original never changes.
Strings are immutable. There is no operation that edits one in place, so s[0] = "X" does nothing useful (and throws in strict mode). Anything that looks like a change is a new string plus an assignment.
Three ways to quote, and when each wins
const single = 'the usual choice';
const double = "handy when the text has an apostrophe: don't";
const name = 'Ada';
const tpl = `Hello ${name}, you have ${1 + 1} messages`;
console.log(tpl);
console.log(`multi
line works too`);
- Template literals interpolate with
${...}and may contain real newlines. - Any expression works inside the braces, including a function call or a ternary.
- They are the only quoting style that survives a refactor without escaping gymnastics.
- Nesting is legal but hurts to read. Extract a variable instead.
Escapes
console.log('line one\nline two'); // \n is a newline
console.log('a\tb'); // \t is a tab
console.log('back\\slash'); // \\ is one backslash
console.log('quote: \'inside\''); // escape the quote you used
console.log('\u00e9 \u{1F642}'); // by code unit, and by code point
Length is code units, not characters
Strings are stored as UTF-16 code units. Characters outside the basic range, such as emoji, take two units, so .length counts storage rather than what a reader would call characters. Spreading or Array.from iterates by code point instead.
const face = '\u{1F642}'; // a single emoji
console.log(face.length); // 2, two UTF-16 code units
console.log([...face].length); // 1, one code point
console.log(face.codePointAt(0)); // 128578
console.log(face[0]); // half a character, unusable on its own
The methods worth memorising
| Method | Does | Note |
|---|---|---|
at(i) | one character, negative index allowed | at(-1) is the last one |
slice(start, end) | a section, negatives count from the end | prefer it over substring |
padStart(n, ch) | pads on the left to length n | zero padding a time or an id |
trim() | removes surrounding whitespace | also trimStart and trimEnd |
replaceAll(a, b) | every occurrence | replace only does the first |
split(sep) | string to array | split("") splits by code unit, so beware emoji |
includes(s) | boolean containment | also startsWith and endsWith |
localeCompare(b) | human sort order | the only correct way to sort names |
normalize() | canonical unicode form | compare text from different sources |
const raw = ' user-42, admin-7 ';
console.log(raw.trim().split(', ')); // ['user-42','admin-7']
console.log(raw.includes('admin')); // true
console.log('7'.padStart(3, '0')); // '007'
console.log('a-b-c'.replaceAll('-', '+')); // 'a+b+c'
console.log('hello'.at(-1), 'hello'.slice(-3)); // 'o' 'llo'
const names = ['Zoe', 'ake', 'Ada', '\u00e9clair'];
console.log([...names].sort()); // capitals first, accents last
console.log([...names].sort((a, b) => a.localeCompare(b))); // human orderSorting names: the default sort compares code units, which is not alphabetical for humans.
const composed = 'caf\u00e9'; // e with an acute accent, one code point
const decomposed = 'cafe\u0301'; // plain e plus a combining accent
console.log(composed === decomposed); // false
console.log(composed.length, decomposed.length); // 4 5
console.log(composed.normalize() === decomposed.normalize()); // trueTwo strings that look identical and are not.
const face = '\u{1F642}';
console.log(face.length, [...face].length);.length counts UTF-16 code units and this emoji needs two of them, so it reports 2. Spreading iterates by code point, which gives 1. Neither number is wrong, they answer different questions: storage size versus character count.
Try it yourself
String lab
const input = ' Ada Lovelace, 1815 ';
const clean = input.trim();
const [name, year] = clean.split(', ');
console.log('name:', name);
console.log('initials:', name.split(' ').map((w) => w[0]).join(''));
console.log('year:', Number(year), 'padded:', String(year).padStart(6, '0'));
console.log('shouty:', name.toUpperCase(), 'last char:', name.at(-1));
console.log('code points:', [...name].length, 'code units:', name.length);
Add an emoji to input and rerun. Which lines still behave, and which start reporting nonsense?
Exercises
Truncate with an ellipsis
Write truncate(text, max) which returns text unchanged when it is max characters or shorter. When it is longer, cut it so that the result, including a trailing "...", is exactly max characters long.
Count characters a human would count
Write countCharacters(text) which returns the number of code points in text, so an emoji counts as one, not two. .length will not do here.
Check yourself
- What does this log?
- 'hi..' —
trimremoves the surrounding spaces and returns"hi", thenpadEnd(4, ".")appends until the length is 4, giving"hi..". Both methods return new strings, so the original literal is untouched. - You call
s.replace("a", "b")and then logs. What has changed? - Nothing,
replacereturns a new string — Strings cannot be edited in place, soreplacehands back a new string andsis unchanged unless you reassign it. Also worth noting:replacewith a string pattern only changes the first match, which is whatreplaceAllis for. - Why can two visually identical strings fail
===? - They may use different unicode representations of the same character, so you need
normalize()— An accented character can be one composed code point or a base letter plus a combining mark. They render the same and compare as different.normalize()puts both into a canonical form, which is why you normalise text at the boundary of your system.
Common mistakes
- Expecting a string method to modify the string. They all return new strings.
- Using
.lengthas a character count. Emoji and many scripts take more than one code unit. - Sorting names with a bare
sort(), which orders by code unit and puts every capital before every lowercase letter.
Takeaways
- Strings are immutable; every method returns a new one.
- Template literals interpolate and span lines, which makes them the default for anything built from parts.
.lengthcounts UTF-16 code units, spreading counts code points.localeComparefor sorting,normalizefor comparing text from different sources.