Lesson 11 of 30

JavaScript String Methods

Strings Are Values You Cannot Change

A string is text. You write one with single quotes, double quotes or backticks, and the three are interchangeable except that backticks also unlock template literals, which this lesson finishes with. Pick one style for a project and stay with it — most JavaScript codebases use single quotes for plain text and backticks whenever a value has to be inserted.

One property explains every string method in this lesson: strings are immutable. You cannot change a character in place. name[0] = 'X' does nothing whatsoever, and in ordinary non-strict code it does not even throw. Every string method therefore returns a new string and leaves the original exactly as it was. Forgetting to catch that return value is the most common mistake here: a line reading text.trim(); on its own accomplishes precisely nothing, and nothing warns you.

You reach a character by index, exactly as with an array, and length counts characters. at(-1) gives the last character without the arithmetic. Reading past the end returns undefined rather than throwing, which is the same forgiving-until-it-is-not behaviour you saw with arrays.

Some characters cannot be typed directly and need an escape: \n for a line break, \t for a tab, \' for a quote inside a same-quoted string, and a doubled backslash for a backslash itself. The last one matters as soon as you write a Windows file path or a regular expression, because a single backslash there means something else entirely.

Example
const name = 'Ananya Sharma';

console.log(name.length);      // 13
console.log(name[0]);          // 'A'
console.log(name.at(-1));      // 'a'
console.log(name[99]);         // undefined - not an error

// Immutable: this line silently does nothing
name[0] = 'X';
console.log(name);             // 'Ananya Sharma'

// Methods return a NEW string
const messy = '  hello  ';
messy.trim();                  // result discarded - a very common mistake
console.log(messy);            // '  hello  '  (unchanged)

const clean = messy.trim();    // catch the return value
console.log(clean);            // 'hello'

// Escapes
console.log('Line one\nLine two');
console.log('It\'s fine');
console.log("It's fine");      // or simply switch quote style
Notes
  • Because const protects the binding and strings cannot be modified anyway, a string in a const is genuinely fixed — which is not true of an array or object in a const. This is the one case where const means what people expect it to mean.

Finding Things Inside a String

includes answers the yes-or-no question and is what you want most of the time: email.includes('@'). startsWith and endsWith do the same job for the two ends, and both read far more clearly than comparing the result of a slice.

indexOf returns the position of the first occurrence, or -1 when there is none. Use it when you genuinely need the position — usually because the next step is a slice at that point. lastIndexOf searches from the other end, which is how you find a file extension or the last dot in a domain name.

That -1 is a classic trap. if (text.indexOf('a')) is true when the letter is missing, because -1 is truthy, and false when the letter is the very first character, because 0 is falsy. It is wrong in both directions at once, which makes it hard to notice from a single test. Write indexOf(...) !== -1, or avoid the whole question with includes.

All of these are case-sensitive. Searching text a user typed almost always means lowercasing both sides first, and doing it in one place rather than at every comparison. The standard search-box pattern is to trim and lowercase the query once, then compare it against a lowercased copy of each item.

Example
const email = 'ananya.sharma@example.com';

console.log(email.includes('@'));         // true
console.log(email.startsWith('ananya'));  // true
console.log(email.endsWith('.com'));      // true

console.log(email.indexOf('@'));          // 13
console.log(email.indexOf('#'));          // -1
console.log(email.lastIndexOf('.'));      // 21

// The -1 trap - both of these answer backwards
console.log(Boolean('abc'.indexOf('z'))); // true  - but 'z' is NOT there
console.log(Boolean('abc'.indexOf('a'))); // false - but 'a' IS there
console.log('abc'.indexOf('a') !== -1);   // true  - correct
console.log('abc'.includes('a'));         // true  - correct and clearer

// Case-sensitive: normalise once, then compare
const query = '  ANANYA ';
const list = ['Ananya Sharma', 'Rahul Verma'];
const q = query.trim().toLowerCase();
console.log(list.filter(n => n.toLowerCase().includes(q)));
// ['Ananya Sharma']
Notes
  • includes, startsWith and endsWith all take an optional second argument for where to start looking. You will rarely need it, but it is there when you are scanning a long string in stages.

Extracting Parts: slice, split and join

slice(start, end) takes a section out and returns it as a new string. The end index is exclusive, and negative numbers count back from the end, so slice(-3) gives the last three characters. This is the same slice you met on arrays with the same rules, which is one fewer thing to memorise.

substring looks almost identical and behaves differently in two ways that eventually catch everyone: it treats a negative argument as 0, and if you pass the two arguments the wrong way round it silently swaps them rather than returning an empty string. Neither is behaviour you want in code you are debugging at midnight. Use slice, and treat substring as something you read in older code.

split turns a string into an array by cutting at a separator, and join turns an array back into a string. Together they are the standard way to reformat text — split a full name on a space, split a CSV line on a comma, split a date on a slash, rearrange, then join it back. split('') with an empty separator gives one array element per character.

One trap with split is worth knowing before it costs you an hour: splitting an empty string gives [''] — an array containing one empty string, not an empty array. So ''.split(',').length is 1, and any naive count of comma-separated items is off by one whenever the input is blank. Check for an empty string before you split, or filter the result.

Example
const email = 'ananya.sharma@example.com';

console.log(email.slice(0, 13));   // 'ananya.sharma'
console.log(email.slice(14));      // 'example.com'
console.log(email.slice(-3));      // 'com'

// Cut at a position you found rather than one you counted
const at = email.indexOf('@');
console.log(email.slice(0, at));   // 'ananya.sharma'
console.log(email.slice(at + 1));  // 'example.com'

// split and join
const fullName = 'Ananya Sharma';
const parts = fullName.split(' ');
console.log(parts);                        // ['Ananya', 'Sharma']
console.log(parts.reverse().join(', '));   // 'Sharma, Ananya'

console.log('12/08/2026'.split('/'));      // ['12', '08', '2026']
console.log('abc'.split(''));              // ['a', 'b', 'c']

// The empty-string trap
console.log(''.split(',').length);         // 1, not 0
console.log('a,,b'.split(','));            // ['a', '', 'b'] - blanks are kept
Notes
  • split also accepts a limit as a second argument, and a regular expression as the separator. 'a1b2c'.split(/[0-9]/) gives ['a', 'b', 'c'], which is handy when the separator is not always the same character.

Cleaning Up and Changing Case

trim removes whitespace from both ends, and it belongs on essentially every value you read from a form. Users paste text with trailing spaces constantly, and a single stray space is why a login fails, why a search finds nothing, or why a duplicate record appears in a database. trimStart and trimEnd handle one side each when you need that.

toLowerCase and toUpperCase convert case. Lowercasing is the standard way to make a comparison case-insensitive, and it is what you should do to an email address before storing or comparing it. Neither is a display tool, though — if you want an all-capitals heading, that is text-transform in CSS, which leaves the underlying data intact for searching and copying.

padStart and padEnd extend a string to a given length by adding a filler character at one end. The everyday use is leading zeros: String(7).padStart(2, '0') gives '07', which is what clock displays, invoice numbers and roll numbers all need. repeat does exactly what it says and is useful for separators and simple text progress bars.

A composition worth building into your fingers: value.trim().toLowerCase(). Nearly every piece of user-typed text you are going to compare or store should pass through both, once, at the point where you read it. Normalising at the boundary means every line after that can trust the value — the same principle as converting form input to a number the moment you read it rather than at each use.

Example
const typed = '  Ananya.Sharma@Example.COM  ';

const email = typed.trim().toLowerCase();
console.log(email);                        // 'ananya.sharma@example.com'
console.log(typed.length, email.length);   // 29 25

console.log('pass'.toUpperCase());         // 'PASS'

// padStart: leading zeros for times, invoices, roll numbers
console.log(String(7).padStart(2, '0'));   // '07'
console.log(String(42).padStart(6, '0'));  // '000042'

const h = String(9).padStart(2, '0');
const m = String(5).padStart(2, '0');
console.log(`${h}:${m}`);                  // '09:05'

// padEnd lines up fixed-width output
console.log('Maths'.padEnd(12, '.') + '82');   // 'Maths.......82'

// repeat
console.log('-'.repeat(20));               // '--------------------'
Notes
  • trim only removes whitespace from the ends. Runs of spaces in the middle survive it untouched — collapsing those needs replace, which is next.

replace, replaceAll and a First Look at Patterns

replace swaps one piece of text for another and returns a new string. Its famous surprise: when you give it a plain string to search for, it replaces only the first occurrence and leaves the rest alone. That is almost never what people expect, and because the output looks nearly right it can survive a quick test.

replaceAll, added in ES2021, does what the name says and is what you should reach for. The older solution was to pass a regular expression carrying the global flag — /x/g — which still works and which you will meet constantly in existing code, so it is worth being able to read.

A regular expression is a small pattern language for describing text. You do not need to learn it now, but three pieces will carry you a long way. /pattern/g means "every match, not just the first". /pattern/i means "ignore case". And \s+ matches one or more whitespace characters, which is how you collapse the runs of spaces and line breaks that appear whenever a user pastes text from a document.

One safety point that matters the moment user text reaches a page: replacing characters is not a substitute for proper escaping. Building an HTML string by replacement and assigning it to innerHTML is how cross-site scripting bugs are created, because anyone who types a script tag into your form gets their code run on your page. Lesson 16 covers the safe approach, which is to set textContent and let the browser treat the value as text.

Example
const line = 'one,two,three';

console.log(line.replace(',', ' - '));      // 'one - two,three'  <- first only
console.log(line.replaceAll(',', ' - '));   // 'one - two - three'
console.log(line.replace(/,/g, ' - '));     // same result, the older way

// Case-insensitive replacement
console.log('Hello hello'.replace(/hello/gi, 'hi'));   // 'hi hi'

// Collapse runs of whitespace - very common when cleaning pasted text
const messy = 'Ananya    Sharma\n   Class 12';
console.log(messy.replace(/\s+/g, ' '));    // 'Ananya Sharma Class 12'

// Strings are immutable, so the original is always still there
console.log(line);                          // 'one,two,three'
Notes
  • replaceAll throws a TypeError if you hand it a regular expression without the g flag. That is deliberate — it stops you writing something whose name and behaviour would contradict each other.

Template Literals

Backticks create a template literal, and they solve two problems at once. First, ${...} inserts a value straight into the text, so you stop assembling sentences out of + signs and carefully placed quote marks. Second, a template literal can span several lines exactly as you typed them, with no escape characters at all.

Anything can go inside ${} — a variable, a property lookup, a function call, arithmetic, a ternary. Keep it short, though. A template literal containing a three-clause expression is harder to read than the two lines it replaced. Calculate above, insert below.

Whatever you insert is converted to a string automatically, following the same rules as String(value). That is convenient, and it is also why a mistyped property name shows up on your page as the literal word "undefined" instead of raising an error. If you ever see that word rendered on a real page, look for a property that does not exist — and use ?? to supply something sensible in its place.

Template literals are also how you build small pieces of HTML in code, usually with map and join. Do that only with data you control. As soon as user-typed text is involved, inserting it into an HTML string and assigning to innerHTML hands control of your page to whoever typed it. Lesson 16 covers the safe alternatives properly.

Example
const name = 'Ananya';
const marks = 82;
const total = 235;

// Insert values, with no + and no quote juggling
console.log(`${name} scored ${marks} out of 100`);
console.log(`Percentage: ${(total / 300 * 100).toFixed(1)}%`);   // '78.3%'
console.log(`Result: ${marks >= 40 ? 'Pass' : 'Fail'}`);

// Multi-line, exactly as written
const card = `Name: ${name}
Marks: ${marks}
Status: ${marks >= 40 ? 'Pass' : 'Fail'}`;
console.log(card);

// A missing property becomes the word "undefined"
const student = { name: 'Rahul' };
console.log(`Marks: ${student.mark}`);         // 'Marks: undefined'
console.log(`Marks: ${student.mark ?? '-'}`);  // 'Marks: -'

// Building rows from data you control
const subjects = ['Maths', 'Physics'];
console.log(subjects.map(s => `<li>${s}</li>`).join(''));
  • trim() — strip whitespace from both ends; use it on every form value
  • toLowerCase() — normalise before comparing or storing
  • includes() — is this text present; prefer it over indexOf() !== -1
  • slice(start, end) — take a section; end is exclusive; negatives count from the end
  • split() and join() — string to array and back again
  • replaceAll() — every occurrence; replace() does only the first
  • padStart() — leading zeros for times, invoices and ids
  • Backticks and ${} — insert values and write multi-line text
Notes
  • Template literals do not add spaces for you. `${a}${b}` runs the two values straight together, so every space in the output has to be a space you typed inside the backticks.
Ask AI