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

MethodDoesNote
at(i)one character, negative index allowedat(-1) is the last one
slice(start, end)a section, negatives count from the endprefer it over substring
padStart(n, ch)pads on the left to length nzero padding a time or an id
trim()removes surrounding whitespacealso trimStart and trimEnd
replaceAll(a, b)every occurrencereplace only does the first
split(sep)string to arraysplit("") splits by code unit, so beware emoji
includes(s)boolean containmentalso startsWith and endsWith
localeCompare(b)human sort orderthe only correct way to sort names
normalize()canonical unicode formcompare 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 order

Sorting 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()); // true

Two 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..' — trim removes the surrounding spaces and returns "hi", then padEnd(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 log s. What has changed?
Nothing, replace returns a new string — Strings cannot be edited in place, so replace hands back a new string and s is unchanged unless you reassign it. Also worth noting: replace with a string pattern only changes the first match, which is what replaceAll is 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 .length as 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.
  • .length counts UTF-16 code units, spreading counts code points.
  • localeCompare for sorting, normalize for comparing text from different sources.