Module 3 · Strings and Template Literals
Every String Method, One by One
In this lesson
- Call every string method a working programmer uses, from
attolocaleCompare, and predict what each one gives back. - State the cost of each method as
O(1),O(k),O(n)orO(n + k), and say what work makes that cost. - Explain why
if (s.indexOf(x))is a bug, read any character's code from the ASCII table, and build output lines with template literals.
Bob builds the search box for Zara's online bookshop. A title matches when it contains the word the reader typed. He tests it with Java, and Learn Java is found. Then the bug reports arrive: Java for Kids is "not found", and so is the first word of every other title. Bob wrote if (title.indexOf(word)), and the search section of this lesson explains why that line fails.
Lesson 01 showed a string as a row of boxes, the code units, numbered from 0. A method is a function that belongs to a value: you call it with a dot, as in s.trim(). This lesson visits every string method a working programmer uses, one family at a time. Each family gets its form, a small program, its real output, and its cost.
It is the longest lesson of the module, so take it in two sittings if you like: split and join is the halfway point. Each family stands on its own, and the six you will type every day are slice, split, join, trim, includes and template literals.
How to read a cost line
Every family below ends with a one-row table: the call, its cost, and why. The cost says how the work grows as the string grows, not how many milliseconds it takes. It is written in big-O notation, with two letters:
- n is the length of the string you call the method on, in code units.
- k is the length of the result, or of the text you search for.
| Cost | Read it as | Example |
|---|---|---|
O(1) | the same small amount of work at any length | s.length on 5 code units or on 5 million |
O(k) | work that grows with the result | s.slice(0, 3) copies 3 code units |
O(n) | work that grows with the whole string | s.toUpperCase() visits every code unit |
O(n + k) | one walk through the string, plus the search text or the result | s.indexOf(t) looks for t in s |
ECMAScript, the language standard, says what each method returns, not how fast it runs. So each cost line gives the size of the job, as V8, the engine inside Node 22, does it. V8 does less in two places (a long slice, and +) and more in one; those lines say so. So read O(n) as "doubling the string doubles the work".
The whole set on one map
Here is every form this lesson teaches, on one card. Each one has its own section below, in this order.
The string methods, by family
read one unit s.length s[i] s.at(i) s.charAt(i)
cut a piece s.slice(start, end) s.substring(start, end)
search s.indexOf(t) s.lastIndexOf(t) s.includes(t)
ends s.startsWith(t) s.endsWith(t)
case s.toUpperCase() s.toLowerCase()
spaces s.trim() s.trimStart() s.trimEnd()
apart, together s.split(sep) parts.join(sep)
replace s.replace(a, b) s.replaceAll(a, b)
longer s.padStart(w, fill) s.padEnd(w, fill) s.repeat(count) s.concat(t)
codes s.charCodeAt(i) s.codePointAt(i) String.fromCharCode(code)
compare s < t s > t s.localeCompare(t)
build `text ${expression} text`
sis any string;tis another string;i,start,end,wandcountare numbers.s.lengthhas no parentheses: it is a stored value, a property, not a method you call.
The map below sorts the same methods by what they give back. Knowing the type of the answer tells you what you can do with it next.
So the type of the answer decides the next step. A number can be compared or used as an index. A boolean goes in an if, and an array can be joined back into a string.
length, s[i], at() and charAt(): reading one box
Reading one code unit
s.length how many code units
s[i] the code unit at index i, or undefined past the end
s.at(i) the same, and a negative i counts from the end
s.charAt(i) the same as s[i], but "" past the end
const name = "Kenji";
console.log(name.length);
console.log(name[0], name[4], name.at(-1), name.at(-2));
console.log(name[5], name.at(5));
console.log("[" + name.charAt(5) + "]");
5
K i i j
undefined undefined
[]
These are lesson 01's tools, here for completeness. at(-1) is the last code unit and at(-2) the one before it. charAt is the oldest of the three: it gives an empty string past the end, where s[i] and at give undefined.
| Call | Cost | Because |
|---|---|---|
s.length, s[i], s.at(i), s.charAt(i) | O(1) | the length is stored, and a code unit's place is the start plus i; lesson 05 shows the one exception, a string just built by a long += loop |
The one thing to watch: length counts code units, not the characters a reader sees (lesson 01's "মাহি 😀" has length 7). So every index in this lesson is a code-unit index.
slice and substring: cutting out a piece
Cutting a piece
s.slice(start, end) from start up to end, end left out; negatives count from the end
s.slice(start) from start to the end of the string
s.substring(start, end) the same piece, but it swaps start and end if start is bigger,
and it reads a negative number as 0
- Both return a new string. The code unit at
endis never included, so the piece hasend - startcode units.
const lang = "JavaScript";
console.log(lang.slice(4));
console.log(lang.slice(-6));
console.log(lang.slice(0, 4));
console.log(lang.slice(4, -2));
console.log("[" + lang.slice(4, 1) + "]");
console.log(lang.substring(4, 1));
console.log(lang.substring(-3));
console.log(lang.substr(4, 3));
console.log("num:42".slice(4));
Script
Script
Java
Scri
[]
ava
JavaScript
Scr
42
The two methods agree on the first three kinds of call and disagree on the rest. Step through them on the boxes, and watch which ones each call picks.
So slice reads a negative number as "count from the end", and an end before the start as "nothing". substring reads a negative as 0 and quietly swaps a start that is too big. The slice rules are easier to predict, and this track writes slice. The last line keeps Module 2's promise: token.slice(4) is the text after the first four code units. So "num:42" gives "42".
The line with substr shows the third method you will meet in old code. Its second number is a length, not an end: substr(4, 3) takes three code units from index 4. The standard keeps it in Annex B, its appendix of features kept only so old web pages still work. Annex B asks new code not to use them. Write slice(4, 7) instead.
| Call | Cost | Because |
|---|---|---|
s.slice(a, b), s.substring(a, b) | at most O(k) | the k code units of the piece are copied; for a long piece V8 may share the old string's memory instead of copying (lesson 05) |
The one thing to watch: the end is left out. Bob wants the first three letters of "Kenji" and writes slice(0, 2), which gives "Ke". The piece from a up to b has b - a code units, so the first three are slice(0, 3).
indexOf, lastIndexOf and includes: searching
Searching
s.indexOf(t) the index where t first starts in s, or -1
s.indexOf(t, from) the same, starting the search at index from
s.lastIndexOf(t) the index where t last starts, or -1
s.includes(t) true if t is somewhere in s, otherwise false
const title = "JavaScript for Kids";
console.log(title.indexOf("Java"), title.indexOf("for"), title.indexOf("Python"));
console.log(title.indexOf("i"), title.lastIndexOf("i"), title.indexOf("i", 8));
console.log(title.includes("Kids"), title.includes("kids"));
console.log(title.indexOf("Java") !== -1, "Python Basics".includes("Java"));
0 11 -1
7 16 16
true false
true false
indexOf answers "where": the index of the first match. When there is no match it answers -1, a number that can never be an index. includes answers "is it there" with a boolean. Both are case-sensitive, so "kids" is not found in a title that says Kids.
Now Bob's search box. Here are three titles checked his way, with the number indexOf gave beside each verdict.
const word = "Java";
const a = "Java for Kids";
const b = "Learn Java";
const c = "Python Basics";
console.log(a.indexOf(word), a.indexOf(word) ? "found" : "not found");
console.log(b.indexOf(word), b.indexOf(word) ? "found" : "not found");
console.log(c.indexOf(word), c.indexOf(word) ? "found" : "not found");
0 not found
6 found
-1 found
Every verdict depends on whether the number is truthy (Module 2). A match at the start gives 0, and 0 is falsy, so the first title is "not found". A missing word gives -1, and -1 is truthy, so Python Basics is "found". Bob's line is wrong both ways. Ask the real question: title.includes(word), or title.indexOf(word) !== -1.
| Call | Cost | Because |
|---|---|---|
s.indexOf(t), s.lastIndexOf(t), s.includes(t) | O(n + k) | the search walks s once; a naive search could retry every position, but for longer search texts V8 uses the Boyer-Moore method, which skips ahead. That is the usual cost: no engine promises it, and a search text built to defeat it can cost more |
The one thing to watch: never use the number from indexOf as a yes-or-no answer. Use includes for "is it there", and keep indexOf for "where".
startsWith and endsWith: checking the ends
Checking the ends
s.startsWith(t) true if s begins with t
s.startsWith(t, at) true if t starts at index at
s.endsWith(t) true if s ends with t
const file = "report-2026.pdf";
console.log(file.startsWith("report"), file.endsWith(".pdf"));
console.log(file.endsWith(".PDF"), file.toLowerCase().endsWith(".pdf"));
console.log(file.startsWith("2026", 7), file.startsWith("2026"));
true true
false true
true false
These two say exactly what they test, so a reader never has to work out an index. The second line shows the usual partner: lower the case first, then check, and .PDF passes too. Calling one method on the result of another is called chaining.
| Call | Cost | Because |
|---|---|---|
s.startsWith(t), s.endsWith(t) | O(k) | only the k code units of t are compared, at one known place; the rest of s is never read |
The one thing to watch: a file name's ending is not proof of what is inside the file. Use endsWith to sort names, not to trust uploads.
toUpperCase and toLowerCase: changing the case
Changing the case
s.toUpperCase() a new string in capitals
s.toLowerCase() a new string in small letters
const city = "Dhaka";
const loud = city.toUpperCase();
console.log(loud, city);
console.log("ZARA".toLowerCase(), "straße".toUpperCase(), "straße".toUpperCase().length);
console.log("Amara" === "AMARA", "Amara".toLowerCase() === "AMARA".toLowerCase());
DHAKA Dhaka
zara STRASSE 7
false true
city is still Dhaka: the method made a new string and left the old one alone, as lesson 01 promised. The German ß has no single capital, so it becomes SS, and the length grows from 6 to 7. The last line is the everyday use: lower both sides, then compare, and capitals stop mattering. Bangla letters have no case, so these methods leave them unchanged.
| Call | Cost | Because |
|---|---|---|
s.toUpperCase(), s.toLowerCase() | O(n) | every code unit is read and a new string of about n code units is written |
The one thing to watch: the result must be kept. name.toUpperCase(); on a line of its own does nothing you can see (Common mistake 1).
trim, trimStart and trimEnd: removing spaces at the ends
Trimming
s.trim() a new string without the whitespace at both ends
s.trimStart() only the start
s.trimEnd() only the end
- Whitespace means spaces, tabs and line breaks, plus a few rarer space characters.
const typed = " Zara ";
console.log("[" + typed.trim() + "]");
console.log("[" + typed.trimStart() + "]");
console.log("[" + typed.trimEnd() + "]");
console.log("\tok\r\n".length, "\tok\r\n".trim().length);
console.log("[" + "Amara writes".trim() + "]");
[Zara]
[Zara ]
[ Zara]
5 2
[Amara writes]
The brackets make the spaces visible. The fourth line removes a tab (\t) and a Windows line ending (\r\n): 5 code units become 2. The last line shows what trim never touches: spaces in the middle.
| Call | Cost | Because |
|---|---|---|
s.trim(), s.trimStart(), s.trimEnd() | O(n) | it scans in from each end until it meets a non-space; a string of only spaces is scanned whole |
The one thing to watch: a line read with the lines variant keeps a \r at its end if the input came from Windows. trim() or trimEnd() removes it.
split and join: apart and back together
This is the pair you will use most. split cuts a string into an array, an ordered list of values (Module 7 teaches arrays). join is an array method that glues the pieces back into one string.
Apart and together
s.split(sep) an array of the pieces between each sep
s.split(sep, limit) only the first limit pieces
s.split("") one piece per code unit
parts.join(sep) one string: the pieces with sep between them
parts.join() the same, with a comma between them
const csv = "Alice,Bob,Zara";
console.log(csv.split(","));
console.log(csv.split(",", 2));
console.log("Zara".split(","));
console.log("abc".split(""));
console.log(csv.split(",").join(" and "));
console.log(["2026", "10", "07"].join("-"));
console.log(csv.split(",").join());
[ 'Alice', 'Bob', 'Zara' ]
[ 'Alice', 'Bob' ]
[ 'Zara' ]
[ 'a', 'b', 'c' ]
Alice and Bob and Zara
2026-10-07
Alice,Bob,Zara
Node prints an array in square brackets, with each string in single quotes. A string with no separator in it still gives an array, of one piece. split("") gives one piece per code unit, which is fine for plain English letters. An emoji is two code units, so split("") cuts it in half; lesson 04 shows the safe way.
Now Module 1's starter line, which you have pasted into every program: input.split(/\s+/).filter(Boolean). Here is why it is written that way, run on a small input with extra spaces.
const input = " 3\n10 20 30\n";
console.log(input.split(" "));
console.log(input.split(/\s+/));
console.log(input.split(/\s+/).filter(Boolean));
[ '', '', '3\n10', '', '20', '30\n' ]
[ '', '3', '10', '20', '30', '' ]
[ '3', '10', '20', '30' ]
Piece 1, why not split(" "). A single space as the separator fails twice. Two spaces in a row leave an empty piece between them. And a line break is not a space, so '3\n10' stays stuck together.
Piece 2, /\s+/. The separator between the slashes is a regular expression, a pattern rather than a fixed text. \s means any whitespace character, and + means "one or more in a row". So any run of spaces, tabs and line breaks counts as one cut. Module 16 teaches regular expressions properly.
Piece 3, the empty ends. The input starts with spaces and ends with a line break. A cut at the very start leaves an empty piece before it, and a cut at the very end leaves one after it. That is the '' at each end of the second line.
Piece 4, filter(Boolean). filter is an array method (Module 7) that keeps the pieces a test says yes to. The test here is Boolean, Module 2's converter, passed in as a value (Module 5 shows functions passed this way). An empty string is falsy, so every '' is dropped. So the starter line means: cut at every run of whitespace, then throw away the empty pieces.
| Call | Cost | Because |
|---|---|---|
s.split(sep), parts.join(sep) | O(n + k) | split walks s once and copies the pieces; join copies every piece once into a result of k code units |
The one thing to watch: split returns an array, not a string. To get a string back, finish with join.
replace and replaceAll: swapping text
Replacing
s.replace(a, b) a new string with the FIRST a changed to b
s.replaceAll(a, b) a new string with EVERY a changed to b
const msg = "red, red, red";
console.log(msg.replace("red", "blue"));
console.log(msg.replaceAll("red", "blue"));
console.log(msg);
const price = "1,299";
console.log(Number(price));
console.log(Number(price.replaceAll(",", "")));
console.log(Number("1,299,000".replaceAll(",", "")) + 1);
blue, red, red
blue, blue, blue
red, red, red
NaN
1299
1299001
With a string as its first argument, replace changes only the first match. replaceAll, added in ES2021, changes them all. msg itself never changed. The second half keeps Module 2's promise about "1,299": replace every comma with nothing, then convert, and the price is 1299.
| Call | Cost | Because |
|---|---|---|
s.replace(a, b), s.replaceAll(a, b) | O(n + k) | a search through s, as with indexOf, then a new string of k code units is built |
The one thing to watch: replace with a string is "replace the first". When you mean "replace all", write replaceAll (Common mistake 2).
padStart and padEnd: lining up columns
Padding
s.padStart(w) s with spaces added on the left until it is w code units long
s.padStart(w, fill) the same, using fill instead of a space
s.padEnd(w) spaces added on the right instead
console.log("[" + "7".padStart(3) + "]");
console.log("[" + "7".padEnd(3) + "]");
console.log("42".padStart(5, "0"), "5".padStart(4, "ab"));
console.log("Zara".padStart(2));
[ 7]
[7 ]
00042 aba5
Zara
A fill longer than one code unit repeats and is cut to fit. A string already as long as w comes back unchanged, never cut. These are string methods, so a number is turned into a string first with String(x). Module 1 promised this, for a times table with its columns lined up.
const n = 12;
const width = String(n * 10).length;
for (let i = 1; i <= 10; i++) {
console.log(n + " x " + String(i).padStart(2) + " = " + String(n * i).padStart(width));
}
12 x 1 = 12
12 x 2 = 24
12 x 3 = 36
12 x 4 = 48
12 x 5 = 60
12 x 6 = 72
12 x 7 = 84
12 x 8 = 96
12 x 9 = 108
12 x 10 = 120
The widest product is n * 10, so its length is the column's width, 3 here. Every i is padded to 2 and every product to 3, so the = signs stand in one column. The for loop is Module 1's; Module 4 teaches it.
| Call | Cost | Because |
|---|---|---|
s.padStart(w), s.padEnd(w) | O(k) | a new string of k code units (k is w, or n when s is already longer) is written |
The one thing to watch: padding counts code units. A Bangla word or an emoji lines up by its length, not by its width on screen.
repeat: the same text many times
Repeating
s.repeat(count) s written count times in a row; count must be 0 or more
console.log("=".repeat(12));
console.log("ab".repeat(3), "[" + "ab".repeat(0) + "]");
console.log("-".repeat(2.9));
console.log("*".repeat(-1));
============
ababab []
--
RangeError: Invalid count value: -1
A count with a fraction is cut down to a whole number, so 2.9 counts as 2. A negative count has no meaning, and the program stops with a RangeError, the error for a number outside the allowed range. The Playground's verdict is Runtime error, and the three lines printed before the stop stay.
| Call | Cost | Because |
|---|---|---|
s.repeat(count) | O(k) | the result has k code units, n times count, and each one is written once |
The one thing to watch: a count computed from input can go negative. Check it before you call repeat.
concat and +: two names for joining
Joining two strings
s.concat(t, ...) s followed by every argument, each turned into a string
s + t the same, written as the operator from Module 2
const first = "Ada";
console.log(first.concat(" ", "Lovelace"));
console.log(first + " " + "Lovelace");
console.log("Room ".concat(7), "Room " + 7);
console.log(first);
Ada Lovelace
Ada Lovelace
Room 7 Room 7
Ada
concat and + do the same job, and both turn a number into text first. Almost everyone writes +, or a template literal (the last section). concat is here so you recognise it in someone else's code.
| Call | Cost | Because |
|---|---|---|
s.concat(t), s + t | O(1) to O(k) | short results are copied, k code units; for long ones V8 links the two strings without copying (a rope, lesson 05) |
The one thing to watch: + follows Module 2's rule, so "Room " + 7 + 1 is "Room 71". Group the arithmetic in parentheses.
charCodeAt, codePointAt and String.fromCharCode: characters as numbers
Every code unit is a number. Module 2 said "1" is 49 and "9" is 57, and promised the table. These three calls move between a character and its number.
Character codes
s.charCodeAt(i) the number of the code unit at index i, or NaN past the end
s.codePointAt(i) the number of the whole character starting at i
String.fromCharCode(c, ...) a new string made of the code units c, ...
console.log(" ".charCodeAt(0), "0".charCodeAt(0), "A".charCodeAt(0), "a".charCodeAt(0));
console.log("Zara".charCodeAt(1), "Zara".charCodeAt(9));
console.log(String.fromCharCode(72, 105, 33));
console.log("😀".length, "😀".charCodeAt(0), "😀".codePointAt(0));
console.log(String.fromCharCode("c".charCodeAt(0) - 32), "7".charCodeAt(0) - 48);
32 48 65 97
97 NaN
Hi!
2 55357 128512
C 7
The fourth line is lesson 01's warning again. The emoji is two code units, so charCodeAt(0) sees only its first half, 55357. codePointAt(0) reads both halves together and gives the character's real number, 128512. Lesson 05 explains how the two halves combine.
The last line uses two tricks the table below makes obvious. A small letter minus 32 is its capital, and a digit's code minus 48 is its value. Four numbers are worth learning by heart, because the rest of the table follows from them:
' '(space) is 32.'0'is 48, so'9'is 57.'A'is 65, so'Z'is 90.'a'is 97, so'z'is 122, and every small letter is its capital plus 32.
Here are all 128 codes of ASCII, the American code table from 1963 that Unicode kept as its first 128 entries. Read a column top to bottom: each holds 16 codes in a row. In JavaScript, the code units 0 to 127 are exactly these. If you did the Learn C track, this is the same table as its Module 1 lesson "The Complete ASCII Table, All 128 Rows".
| 0 to 15 | 16 to 31 | 32 to 47 | 48 to 63 | 64 to 79 | 80 to 95 | 96 to 111 | 112 to 127 |
|---|---|---|---|---|---|---|---|
| 0 NUL | 16 DLE | 32 SP | 48 0 | 64 @ | 80 P | 96 ` | 112 p |
| 1 SOH | 17 DC1 | 33 ! | 49 1 | 65 A | 81 Q | 97 a | 113 q |
| 2 STX | 18 DC2 | 34 " | 50 2 | 66 B | 82 R | 98 b | 114 r |
| 3 ETX | 19 DC3 | 35 # | 51 3 | 67 C | 83 S | 99 c | 115 s |
| 4 EOT | 20 DC4 | 36 $ | 52 4 | 68 D | 84 T | 100 d | 116 t |
| 5 ENQ | 21 NAK | 37 % | 53 5 | 69 E | 85 U | 101 e | 117 u |
| 6 ACK | 22 SYN | 38 & | 54 6 | 70 F | 86 V | 102 f | 118 v |
| 7 BEL | 23 ETB | 39 ' | 55 7 | 71 G | 87 W | 103 g | 119 w |
| 8 BS | 24 CAN | 40 ( | 56 8 | 72 H | 88 X | 104 h | 120 x |
| 9 TAB | 25 EM | 41 ) | 57 9 | 73 I | 89 Y | 105 i | 121 y |
| 10 LF | 26 SUB | 42 * | 58 : | 74 J | 90 Z | 106 j | 122 z |
| 11 VT | 27 ESC | 43 + | 59 ; | 75 K | 91 [ | 107 k | 123 { |
| 12 FF | 28 FS | 44 , | 60 < | 76 L | 92 \ | 108 l | 124 | |
| 13 CR | 29 GS | 45 - | 61 = | 77 M | 93 ] | 109 m | 125 } |
| 14 SO | 30 RS | 46 . | 62 > | 78 N | 94 ^ | 110 n | 126 ~ |
| 15 SI | 31 US | 47 / | 63 ? | 79 O | 95 _ | 111 o | 127 DEL |
Codes 0 to 31 and 127 are control characters: signals to a printer or a terminal, not letters. The table gives their short names. SP is the space. Six of them still matter:
- NUL (0) ends a string in C; in JavaScript it is an ordinary code unit, written
"\0". - TAB (9) is the tab,
"\t". - LF (10) is the line break,
"\n", the separator of the lines variant. - CR (13) is
"\r", which Windows puts before every LF. - ESC (27) starts the codes that colour text in a terminal.
- DEL (127) meant "delete" on paper tape: punching every hole erased a wrong character.
| Call | Cost | Because |
|---|---|---|
s.charCodeAt(i), s.codePointAt(i), String.fromCharCode(c) | O(1), and O(k) to build k code units | reading one number at a known index; building a string writes each code unit once |
The one thing to watch: the plus-32 and minus-48 tricks work only on ASCII letters and digits. They mean nothing for é or a Bangla letter.
Comparing strings: <, > and localeCompare
Comparing
s < t true if s comes first in code-unit order
s.localeCompare(t) a negative number, 0, or a positive number, in the order a person expects
console.log("Z" < "a", "10" < "9", "apple" < "apricot");
console.log("ab" < "abc", "Zara" > "Amara", "Amara" === "amara");
console.log("a".localeCompare("b"), "b".localeCompare("a"), "a".localeCompare("a"));
console.log(["banana", "apple", "Cherry", "éclair"].sort());
console.log(["banana", "apple", "Cherry", "éclair"].sort((x, y) => x.localeCompare(y)));
console.log(["banana", "apple", "Cherry", "éclair"].sort(new Intl.Collator("en").compare));
true true true
true true false
-1 1 0
[ 'Cherry', 'apple', 'banana', 'éclair' ]
[ 'apple', 'banana', 'Cherry', 'éclair' ]
[ 'apple', 'banana', 'Cherry', 'éclair' ]
< on two strings compares code by code from the left, and the first difference decides. That answers Module 2's teaser. "Z" is 90 and "a" is 97, so every capital comes before every small letter. "10" < "9" is true because "1" (49) beats "9" (57) and the rest is never read. A string that runs out first is the smaller one.
A person expects apple before Cherry, whatever the capitals. localeCompare follows a language's rules for that order, called its collation. The second-to-last line passes it to an array's sort (Module 7) with an arrow function (Module 5). Intl.Collator builds the same comparison once for reuse, and lesson 04 decides when each one fits.
| Call | Cost | Because |
|---|---|---|
s < t, s.localeCompare(t) | O(n), n the shorter length | < stops at the first difference; localeCompare does more work per character, for the language's rules |
The one thing to watch: two strings of digits compare as text. Convert them with Number() first when you mean numbers.
Template literals in full
Lesson 01 showed a string written between backticks. That form is a template literal, and it can hold values and line breaks.
Template literals
`text ${expression} more text` the value of expression, turned into text, put in its place
`line one
line two` a real line break, kept as it is typed
\` \${ a backtick, or a dollar sign and brace, as plain text
- Any expression fits inside
${ }: a name, a sum, a method call, a ternary, even another template literal.
const name = "Zara";
const books = 3;
const price = 450;
console.log(`${name} bought ${books} books for ${books * price} taka.`);
console.log(`Order type: ${books > 2 ? "bulk" : "single"}`);
const card = `Name: ${name.toUpperCase()}
Books: ${books}`;
console.log(card);
console.log(`Paid: ${`${books * price}`.padStart(6, ".")} taka`);
console.log(`A backtick: \`, and a dollar sign: \${books}`);
Zara bought 3 books for 1350 taka.
Order type: bulk
Name: ZARA
Books: 3
Paid: ..1350 taka
A backtick: `, and a dollar sign: ${books}
Line 1 holds three values with no + at all. Line 2 puts a ternary inside. card spans two lines in the source, and it prints on two lines. The fourth line nests a template inside ${ } to turn the total into text before padding it. The last line shows the two escapes.
A function name written straight before the backtick, such as String.raw, makes a tagged template. The function receives the pieces and decides the result. Libraries use tags for things like database queries. This track names them and does not teach them.
| Call | Cost | Because |
|---|---|---|
`...${x}...` | O(k) | each value is turned into text and every piece is written once into a result of k code units |
The one thing to watch: a template literal keeps every space and line break you type. Indent the second line of card and the indent is printed too.
Amara's photo app needs the type of each file it is given, read from the text after the last dot.
const file = "holiday.photo.JPG";
const dot = file.lastIndexOf(".");
const ext = file.slice(dot + 1).toLowerCase();
console.log(dot, ext);
console.log(ext === "jpg" ? "an image" : "something else");
console.log("README".lastIndexOf("."));
13 jpg
an image
-1
lastIndexOf skips the first dot and finds the one at index 13. slice(dot + 1) starts one past it, and toLowerCase makes JPG and jpg the same. The last line is Zara's first test: a name with no dot gives -1, and slice(0) would then return the whole name. Exercise 3 makes the program handle that.
Maria's newsletter form collects email addresses as people typed them: spaces at the ends, capitals, and sometimes no @ at all. Her program reads one address per line, so it uses the lines variant of the starter.
const input = require("fs").readFileSync(0, "utf8");
const lines = input.split("\n");
let at = 0;
const next = () => lines[at++];
const nextInt = () => Number(next());
const out = [];
const n = nextInt();
for (let i = 0; i < n; i++) {
const email = next().trim().toLowerCase();
const verdict = email.includes("@") ? "ok" : "missing @";
out.push(`${email.padEnd(18)} ${verdict}`);
}
console.log(out.join("\n"));
zara@example.com ok
bob.example.com missing @
alice@mail.com ok
That output is for the input 3, then the three addresses, the first with spaces at both ends. Each address goes through a chain: trim, then toLowerCase. includes asks the yes-or-no question, and a template literal with padEnd lines up the verdicts.
Bob rewrites his search. It reads the word, then n titles, one per line. It ignores capitals, and it says where the word starts.
const input = require("fs").readFileSync(0, "utf8");
const lines = input.split("\n");
let at = 0;
const next = () => lines[at++];
const nextInt = () => Number(next());
const out = [];
const word = next().trim().toLowerCase();
const n = nextInt();
for (let i = 0; i < n; i++) {
const title = next();
const where = title.toLowerCase().indexOf(word);
const verdict = where === -1 ? "not found" : `found at ${where}`;
out.push(`${title.padEnd(22)} ${verdict}`);
}
console.log(out.join("\n"));
Java for Kids found at 0
Learn JavaScript found at 6
Python Basics not found
The JAVA Cookbook found at 4
That output is for the word java, then 4 and the four titles. The first title is found at 0, the case that broke Bob's first version, because the test is now where === -1. Lowering both sides finds JAVA too. The index refers to the lowered title, which has the same length as the original for these English titles.
Where this is used
- Node's path module.
path.extname("index.coffee.md")returns".md", the text from the last dot of the file name, andpath.extname(".bashrc")returns"". It is Example 1's job, built into Node. - The URL API. In browsers and in Node,
new URL("https://example.com/books/strings/02").pathnameis"/books/strings/02". A router splits it on"/"into['', 'books', 'strings', '02']to decide which page to show. - nginx access logs. nginx's default
combinedlog format writes one line per request, fields separated by spaces, the client's address first. A small report script splits each line on spaces. The quoted request in the middle holds spaces too: lesson 04's quoted-comma problem again. - Prices on screen.
new Intl.NumberFormat("en-IN").format(1299000)gives"12,99,000", the Indian grouping. It is the reverse of this lesson'sreplaceAll(",", ""): commas are for people, and a program removes them before it calculates.
Common mistakes
1. Calling a method and throwing its result away.
const name = "zara";
name.toUpperCase();
console.log(name);
const loud = name.toUpperCase();
console.log(loud);
zara
ZARA
No error, and nothing changed. A string method returns a new string, and line 2 drops it on the floor. Keep the result in a name, or use it right away. You will write line 2 because some methods do change their value in place, such as an array's sort (Module 7).
2. Using replace to remove every comma.
const price = "1,299,000";
console.log(price.replace(",", ""));
console.log(Number(price.replace(",", "")));
console.log(Number(price.replaceAll(",", "")));
1299,000
NaN
1299000
The first comma went and the second stayed, so the conversion gives NaN. Use replaceAll. You will write replace because a test price like "1,299" has only one comma, so it passes.
3. A negative index in substring.
const name = "Kenji";
console.log(name.substring(-3));
console.log(name.slice(-3));
Kenji
nji
Bob wanted the last three letters and got the whole name, with no message. substring reads -3 as 0. Use slice(-3). You will mix them up because the two names mean the same thing in plain English.
4. Calling a method on a line that is not there.
const input = require("fs").readFileSync(0, "utf8");
const lines = input.split("\n");
let at = 0;
const next = () => lines[at++];
const nextInt = () => Number(next());
const out = [];
const n = nextInt();
for (let i = 0; i < n; i++) {
out.push(next().trim());
}
console.log(out.join("\n"));
TypeError: Cannot read properties of undefined (reading 'trim')
The input promises 4 names and holds 2. The final line break gives one empty line, and the fourth next() runs past the end and returns undefined. A method call on undefined stops the program, the verdict is Runtime error, and nothing was printed, because the print comes last. Check that n matches the lines you test with. You will meet this when your hand-typed test input is shorter than its first line says.
Amara's blog editor shows a word count under every paragraph. Count the words on each line she sends.
Input. The first line holds n, and n lines of text follow. A word is a maximal run of characters that are not spaces. A line may be empty, may start or end with spaces, and may hold several spaces in a row.
Output. n lines: the number of words on each line of text, in the input order.
Constraints. 1 <= n <= 20000. Each line of text has 0 to 300 printable ASCII characters (codes 32 to 126), and the last one is never empty. Every line ends with a newline.
Sample. This input gives 4, 0, 1 and 4 on four lines. The first line of text has two spaces between writes and every, the second is empty, and the third starts with two spaces.
4
Amara writes every day
hello
one two three four
A line may be empty, so the starter is the lines variant. Its second line is const lines = input.split("\n");, so next() returns one whole line.
const input = require("fs").readFileSync(0, "utf8");
const lines = input.split("\n");
let at = 0;
const next = () => lines[at++];
const nextInt = () => Number(next());
const out = [];
// your code: read with next() and nextInt(), push every line of output to out
// Read n, then n lines. Per line: the number of words.
console.log(out.join("\n"));
Graded as word-count. Before you submit, try an empty line, a line of only spaces, and a word with spaces on both sides.
David prints name badges for a coding club, and the small badges have room only for initials. Turn each full name into its initials.
Input. The first line holds n, and n lines follow, one full name each. A name is 1 to 6 words of ASCII letters, separated by single spaces, with no space at either end.
Output. One line per name: the first letter of every word as a capital, each followed by a dot, with nothing between them.
Constraints. 1 <= n <= 20000. Each word has 1 to 20 letters, in any mix of capitals and small letters. The last line of the input is never empty, and every line ends with a newline.
Sample. Input 3, then ada lovelace, Grace Brewster Murray Hopper and zara, gives A.L., G.B.M.H. and Z. on three lines.
A name holds spaces, so this starter is the lines variant too.
const input = require("fs").readFileSync(0, "utf8");
const lines = input.split("\n");
let at = 0;
const next = () => lines[at++];
const nextInt = () => Number(next());
const out = [];
// your code: read with next() and nextInt(), push every line of output to out
// Read n, then n names. Per name: the initials, each a capital and a dot.
console.log(out.join("\n"));
Graded as initials. Try a one-word name and a name typed all in small letters before you submit.
Zara feeds Amara's photo app from Example 1 the file names that broke it. Make it print each extension, or none when a name has no real extension.
Input. n, then n file names. No name contains a space.
Output. One line per name: the text after its last dot, in small letters. Print none when the name has no dot, when its last dot is its first character, or when nothing follows its last dot.
Constraints. 1 <= n <= 1000. Each name has 1 to 100 characters.
Sample. Input 5 and photo.JPG archive.tar.gz README .bashrc notes. gives jpg, gz, none, none and none on five lines.
const input = require("fs").readFileSync(0, "utf8");
const tokens = input.split(/\s+/).filter(Boolean);
let at = 0;
const next = () => tokens[at++];
const nextInt = () => Number(next());
const out = [];
// your code: read with next() and nextInt(), push every line of output to out
console.log(out.join("\n"));
Not graded on its own. Each none in the sample is a different case. Find the value of lastIndexOf(".") for each before you write a test.
Kenji counts how often a pattern appears in a text, and Maria asks "but which count?". In aaaa, the pattern aa starts at 0, 1 and 2, so it appears 3 times if matches may overlap. Only 2 matches fit side by side without sharing a letter. Print both counts.
Input. Line 1 is the text, which may hold spaces. Line 2 is n. Then n patterns follow, one per line, none of them empty.
Output. One line per pattern: the number of overlapping matches, a space, and the number of matches that do not overlap, counted from the left.
Constraints. The text has 1 to 1000 characters. 1 <= n <= 100. Each pattern has 1 to 20 characters.
Sample. The text aaaa banana, then 3 and the patterns aa, ana and x. That input gives 3 2, 2 1 and 0 0 on three lines.
const input = require("fs").readFileSync(0, "utf8");
const lines = input.split("\n");
let at = 0;
const next = () => lines[at++];
const nextInt = () => Number(next());
const out = [];
// your code: read with next() and nextInt(), push every line of output to out
console.log(out.join("\n"));
Not graded on its own. indexOf(t, from) finds the next match after a position. The two counts differ only in where the next search starts. Which of the two does split count?
Common doubts
Do I need to learn all of these by heart?
No. Learn the families and what each gives back, as the method map shows. The exact argument order is one search away, and an editor shows it as you type. The ones you will type every day are
slice,split,join,trim,includesand template literals.Why are there two of so many things: slice and substring, s[i] and charAt?
JavaScript almost never removes a method, because old web pages still call it. So each new, clearer method sits beside the old one. When two do the same job, this track writes the one with simpler rules. That means
slice,s[i]orat, and+or a template literal.Is indexOf or includes faster?
They do the same search, so the cost is the same,
O(n + k). Choose by the question you are asking:includesfor "is it there",indexOffor "where". Lesson 04 makes that choice with more examples.Why does split("") work on "abc" but not on an emoji?
split("")cuts between code units, and an emoji is two code units, so it is cut in half. English letters are one code unit each, so nothing breaks. Lesson 04 shows[...s], which cuts by whole characters instead.
Key takeaways
- No method changes the string; reading one code unit costs
O(1), and a walk through itO(n)orO(n + k). slice(start, end)leaves out the end and counts negatives from the end;substringswaps and reads negatives as 0;substris legacy (Annex B).indexOfgives a position or -1, soif (s.indexOf(x))is wrong both ways: useincludesfor yes or no.splitcuts a string into an array andjoinglues it back;replacechanges the first match andreplaceAllevery match.charCodeAtandString.fromCharCodeturn characters into codes and back (anchors 32, 48, 65, 97);<compares codes,localeComparea person's order.- A template literal puts any expression's value into text with
${ }and keeps its line breaks. - Go deeper: Under the Hood, immutability, ropes and UTF-16 (Pro).
Next, lesson 03 puts these methods to work in six full programs, from a word counter to a shop receipt.
End of lesson 2
Mark it done, and your progress moves with you.
Next: Full Programs: Strings at Work