Warming up the neural circuits...
By the end of this chapter you will:
slice vs substring vs at)trim, replace, toLowerCaseindexOf !== -1 patternEvery app converts data to text constantly — URLs, user names, email addresses, JSON keys, error messages, class names. 's string methods are your tools for cleaning, extracting, validating, and formatting that text.
The key rule to know before anything else: strings in JavaScript are immutable. A string value can never be changed. Methods like .replace() or .toUpperCase() don't modify the string — they return a brand new string. The original stays exactly the same.
const name = "alice";
const upper = name.toUpperCase();
// upper = "ALICE"
// name = "alice" ← completely unchangedThis is actually a feature, not a limitation. Frameworks like React can detect string changes with a simple === check.
1. Immutability: Strings cannot be changed in place. Every method returns a new string.
2. Zero-based indexing: The first character is at 0. "hello"[0] is "h", "hello"[4] is "o".
3. Negative indexing: Some modern methods (.at(), .slice()) support negative numbers that count from the end. "hello".at(-1) is "o" (the last character).
4. Code units vs. Code points: JavaScript stores strings as UTF-16 sequences. Most characters are 1 unit. Emojis use 2 units — which is why "😀".length returns 2 instead of 1. Methods like [...str] are "emoji-safe" because they work with code points.
React and Vue use old === new to decide if a string changed. Because strings are immutable, this check is instant (no need to compare character by character). This is one reason why string-based is extremely efficient in modern frameworks.
slice(start, end?) — Extract a portionWhat it does: Returns a new string from position start up to (but not including) end. If end is omitted, it goes to the end of the string. Supports negative indices (counting from the end).
Signature: str.slice(startIndex, endIndex?)
const url = "https://example.com/profile";
Rendering diagram…
| Method | Negative index? | If start > end | Recommendation |
|---|---|---|---|
slice(start, end) | ✅ Yes (from end) | Returns "" | Use this by default |
substring(start, end) | ❌ Treated as 0 | Silently swaps | Avoid — confusing |
at(index) | ✅ Yes | N/A (single char) | Best for single character access |
charAt(index) | ❌ Returns "" | N/A | Old API — use at() |
| Mistake | What actually happens | Fix |
|---|---|---|
"😀".length === 1 assumed | .length returns 2 (two UTF-16 units) | [..."😀"].length → 1 |
str.replace("-", ":") to replace all dashes | Only the first dash is replaced | str.replaceAll("-", ":") |
str[0] on an empty string | Returns undefined (no error, but unexpected) | str.at(0) ?? "default" or check str.length first |
if (str.indexOf("x")) as a boolean | indexOf returns 0 (found at start) which is falsy | if (str.includes("x")) — always returns a real boolean |
" ALICE@Example.COM ""alice@example.com""https://github.com/torvalds""torvalds"5"05"true only for .jpg, .jpeg, .png, .gif."photo.jpg", "document.pdf"true, falseslugify function that converts a blog post title to a URL-safe slug."Hello, World! This is a Blog Post""hello-world-this-is-a-blog-post"truncate function that limits a string to n visible characters (not bytes), handles emojis correctly, and appends ... if truncated."Hello 😀 World", maxLength 8"Hello 😀..." — emoji counts as 1 character, not 2What does 'hello world'.split(' ') return?
| Property | Value |
|---|---|
| Returns | New string (a portion of the original) |
| Mutates original | ❌ No (strings are immutable) |
| Negative indices | ✅ Yes — count from end |
| Use when | You need to extract any portion of a string |
Real-world example:
// Truncate a long article title for a card preview
function truncate(text, maxLength) {
if (text.length <= maxLength) return text;
return text.slice(0, maxLength) + '...';
}
truncate('JavaScript Array Methods Masterclass Guide', 30);
// 'JavaScript Array Methods Mast...'at(index) — Get one character (supports negative)What it does: Returns the single character at index. Unlike str[0], it supports negative indices. str.at(-1) is the modern, clean way to get the last character.
const filename = "report.pdf";
filename.at(0); // "r" — first character
filename.at(-1); // "f" — last character
filename.at(-4); // "." — 4th from end
// Old way to get last character (harder to read):
filename[filename.length - 1]; // "f"Why not just use str[index]?
str[-1] always returns undefined in JS (negative bracket notation doesn't work).at() correctly handles negative indicessplit(separator, limit?) — Break into an arrayWhat it does: Divides a string into an ordered array of substrings by searching for separator. This is the bridge between strings and arrays.
// CSV parsing
const csv = "Alice,30,Engineer";
csv.split(","); // ["Alice", "30", "Engineer"]
// Split into words
const sentence = "Hello world today";
sentence.split(" "); // ["Hello", "world", "today"]
// Split into individual characters
"hello".split(""); // ["h", "e", "l", "l", "o"]
// Split with a limit
"a-b-c-d".split("-", 2); // ["a", "b"] — stops at 2| Property | Value |
|---|---|
| Returns | Array of strings |
| Mutates original | ❌ No |
| Use when | You need to parse structured text (CSV, URLs, tags) |
Real-world example:
// Parse comma-separated tags from a form field
const tagInput = " javascript , react, typescript ";
const tags = tagInput
.split(",")
.map(tag => tag.trim()) // remove whitespace from each tag
.filter(tag => tag.length); // remove empty strings
// ["javascript", "react", "typescript"]substring(start, end?) vs slice() — Why to avoid itsubstring is the older version. The key differences:
| Feature | slice | substring |
|---|---|---|
| Negative indices | ✅ Yes (counts from end) | ❌ Treated as 0 |
| If start > end | Returns "" | Silently swaps them |
| Recommendation | Use this | Avoid — confusing behavior |
"hello".slice(-3); // "llo" ← correct
"hello".substring(-3); // "hello" ← treats -3 as 0 (confusing!)
"hello".slice(4, 1); // "" ← returns empty (start > end)
"hello".substring(4, 1); // "ell" ← silently swaps to substring(1, 4)Rule: Always use slice. It's predictable.
replace() vs replaceAll() — Substituting textreplace(pattern, replacement) — replaces only the first match (unless you use a global regex /g).
replaceAll(pattern, replacement) — replaces every occurrence. Modern and unambiguous.
const sentence = "the cat sat on the mat";
sentence.replace("the", "a"); // "a cat sat on the mat" — only first!
sentence.replaceAll("the", "a"); // "a cat sat on a mat" — all matches ✅Both can take a regex as the first argument:
// Remove all non-digit characters (regex with /g flag)
const phone = "(123) 456-7890";
phone.replace(/\D/g, ""); // "1234567890"
phone.replaceAll(/\D/g, ""); // same result| Property | Value |
|---|---|
| Returns | New string with replacements applied |
| Mutates original | ❌ No |
| Use when | You need to substitute text, sanitize input, or reformat data |
Real-world example:
// Generate a URL-safe slug from a blog post title
function slugify(title) {
return title
.toLowerCase()
.trim()
.replaceAll(' ', '-') // spaces to hyphens
.replace(/[^a-z0-9-]
trim(), trimStart(), trimEnd() — Remove whitespaceWhat they do: Remove whitespace (spaces, tabs, newlines) from the ends of a string. Essential for cleaning user input.
const rawInput = " alice@example.com ";
rawInput.trim(); // "alice@example.com" — both ends
rawInput.trimStart(); // "alice@example.com " — left only
rawInput.trimEnd(); // " alice@example.com" — right only| Property | Value |
|---|---|
| Returns | New string without leading/trailing whitespace |
| Mutates original | ❌ No |
| Use when | Cleaning form inputs, responses, file-read data |
padStart() & padEnd() — Pad to a fixed widthWhat they do: Add characters to the start or end until the string reaches a target length. Common for formatting times, IDs, and masked values.
// Format minutes as 2-digit clock display
"5".padStart(2, "0"); // "05"
"12".padStart(2, "0"); // "12" — already 2 chars, no padding needed
// Mask a credit card number (show only last 4)
"4242424242424242
toUpperCase() & toLowerCase() — Change caseconst name = "Alice Smith";
name.toUpperCase(); // "ALICE SMITH"
name.toLowerCase(); // "alice smith"
// Case-insensitive comparison
"HELLO".toLowerCase() === "hello".toLowerCaseTip for sorting: When sorting strings alphabetically, always normalize case first. "banana" < "Cherry" is false because uppercase C (Unicode 67) comes before lowercase b (Unicode 98).
["banana", "Cherry", "apple"].sort((a, b) =>
a.toLowerCase().localeCompare(b.toLowerCase())
);
// ["apple", "banana", "Cherry"]repeat(count) — Repeat a string"ha".repeat(3); // "hahaha"
"*".repeat(5); // "*****"
"-".repeat(20); // "--------------------"includes(searchString, position?) — Does it contain this?Returns true if the string contains searchString, false otherwise. Much more readable than the old indexOf() !== -1 pattern.
const email = "alice@example.com";
email.includes("@"); // true
email.includes("gmail"); // false
email.includes("example", 6); // true — starts searching from index 6| Property | Value |
|---|---|
| Returns | Boolean |
| Case sensitive | ✅ Yes |
| Use when | You want to know if a string is present anywhere |
startsWith(prefix, position?) & endsWith(suffix, length?)const filename = "report-2024.pdf";
filename.startsWith("report"); // true
filename.startsWith("data"); // false
filename.endsWith(".pdf")
Why these beat indexOf:
// Old way (confusing — why !== -1?)
if (str.indexOf("admin") !== -1) { /* ... */ }
// Modern way (clear intent)
if (str.includes("admin")) { /*
indexOf(searchString, fromIndex?) & lastIndexOf()Returns the position (index) of the first match, or -1 if not found. Use these when you need the position, not just a boolean.
const path = "/users/alice/profile";
path.indexOf("/"); // 0 — first slash
path.lastIndexOf("/"); // 13 — last slash
path.indexOf("bob")
localeCompare() — Sort strings properly across languagesWhat it does: Compares two strings in a locale-aware way. Returns:
0 if they're equivalentWhy it matters: Standard < and > comparisons fail with accented characters. "ä" sorts incorrectly without localeCompare.
// ❌ Wrong for international text
["banana", "äpple", "cherry"].sort();
// ["banana", "cherry", "äpple"] — ä goes to end (wrong!)
// ✅ Correct
["banana", "äpple", "cherry"].sort
.length count?JavaScript stores strings as UTF-16 sequences. Most characters are 1 unit wide. Emojis and some special characters use 2 units (a "surrogate pair").
"hello".length; // 5 ← correct
"😀".length; // 2 ← WRONG from a human perspective!
"café".length; // 4 ← depends on encoding of éThe fix: Use the spread operator or Array.from() to get the true character count:
[..."😀"].length; // 1 ← correct!
Array.from("hello 😀").length; // 7 ← correctWhy this matters: If you're enforcing a Twitter-style 280-character limit, counting with .length will be wrong for emoji-heavy text.
match(regex) — Extract matchesWhat it does: Returns matches of a regular expression in the string. With the /g flag, returns all matches. Without it, returns the first match plus capture groups.
// Extract all numbers from a string
const text = "Order 42 has 3 items totalling $199";
const nums = text.match(/\d+/g);
// ["42", "3", "199"]
// Extract named capture groups (no /g flag)
const dateStr = "2024-05-10"
matchAll(regex) — Get all matches with groupsBetter than match(/g) when you need capture groups from multiple matches.
const html = '<a href="/users">Users</a><a href="/about">About</a>';
const linkRegex = /<a href="([^"]+)">([^<]+)<\/a>/g;
for (const match of html.matchAll
search(regex) — Find the position of a regex matchLike indexOf but takes a regex. Returns the index of the first match, or -1.
"hello world 42".search(/\d+/); // 12 — position of "42"
"no numbers here".search(/\d+/); // -1normalize() — Handle "invisible" character differencesSome characters look identical but are stored differently in Unicode. normalize() makes them comparable.
const a = "café"; // é as a single code point
const b = "cafe\u0301"; // e + combining accent mark (looks identical!)
a === b; // false — different bytes!
a.normalize() === b.function CreditCardInput() {
const [value, setValue] = useState('');
const handleChange = (e) => {
const raw = e.target.value
.replace(/\D/g, '') // 1. Strip anything that isn't a digit
.slice(0, 16); // 2. Limit to 16 digits
// 3. Add a space every 4 digits for readability
const formatted = raw.replace(/(\d{4})(?=\d)/g, '$1 ');
setValue(formatted);
};
return (
<input
value={value}
onChange={handleChange}
placeholder="0000 0000 0000 0000"
maxLength={19}
/>
);
}When to use: Any input that needs live formatting — phone numbers, card numbers, postal codes.
str.toUpperCase() on null or undefined | TypeError: Cannot read properties of null | str?.toUpperCase() with optional chaining |
substring with negative indices | Treated as 0, not counted from end | Use slice which handles negatives correctly |
Using == to compare strings | Can cause unexpected coercion | Always use === for string comparison |