Learn JavaScript

Lesson 2 of 9 · Strings and Template Literals

Module 3 · Strings and Template Literals

Every String Method, One by One

FreeReading

In this lesson

  • Call every string method a working programmer uses, from at to localeCompare, and predict what each one gives back.
  • State the cost of each method as O(1), O(k), O(n) or O(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.
CostRead it asExample
O(1)the same small amount of work at any lengths.length on 5 code units or on 5 million
O(k)work that grows with the results.slice(0, 3) copies 3 code units
O(n)work that grows with the whole strings.toUpperCase() visits every code unit
O(n + k)one walk through the string, plus the search text or the results.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`
  • s is any string; t is another string; i, start, end, w and count are numbers.
  • s.length has 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.

The method map: what each string method gives back What each method gives back s.method(...) a new string at(i) charAt(i) slice(a, b) substring(a, b) toUpperCase() toLowerCase() trim() trimStart() trimEnd() replace(a, b) replaceAll(a, b) padStart(w) padEnd(w) repeat(n) concat(t) String.fromCharCode(c) `... ${x} ...` a number length indexOf(t) lastIndexOf(t) charCodeAt(i) codePointAt(i) localeCompare(t) true or false includes(t) startsWith(t) endsWith(t) s < t, s > t s === t an array split(sep) join(sep) on the array gives a string back No method changes s itself. Every string in the first box is a new string (lesson 01).

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.

CallCostBecause
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 end is never included, so the piece has end - start code 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.

The string "JavaScript" is ten boxes, numbered 0 to 9. slice(4) and slice(-6) both take boxes 4 to 9, and slice(0, 4) takes boxes 0 to 3. slice(4, 1) is empty. substring(4, 1) swaps its numbers and takes boxes 1 to 3, and substring(-3) reads -3 as 0 and takes the whole string.

index0123456789
code unitJavaScript
hex004A006100760061005300630072006900700074
ExpressionResultWhat happened
s.slice(4)"Script"One number: from box 4 to the end of the string.
s.slice(-6)"Script"A negative start counts from the end: -6 means length - 6, which is 4. The same six boxes.
s.slice(0, 4)"Java"From box 0 up to box 4, and box 4 itself is left out: four boxes.
s.slice(4, 1)""The end comes before the start, so slice takes nothing and gives the empty string.
s.substring(4, 1)"ava"substring swaps 4 and 1 first, so it takes boxes 1, 2 and 3.
s.substring(-3)"JavaScript"substring reads any negative number as 0, so it takes the whole string.

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.

CallCostBecause
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.

CallCostBecause
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.

CallCostBecause
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.

CallCostBecause
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.

CallCostBecause
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.

CallCostBecause
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.

CallCostBecause
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.

CallCostBecause
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.

CallCostBecause
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.

CallCostBecause
s.concat(t), s + tO(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 1516 to 3132 to 4748 to 6364 to 7980 to 9596 to 111112 to 127
0 NUL16 DLE32 SP48 064 @80 P96 `112 p
1 SOH17 DC133 !49 165 A81 Q97 a113 q
2 STX18 DC234 "50 266 B82 R98 b114 r
3 ETX19 DC335 #51 367 C83 S99 c115 s
4 EOT20 DC436 $52 468 D84 T100 d116 t
5 ENQ21 NAK37 %53 569 E85 U101 e117 u
6 ACK22 SYN38 &54 670 F86 V102 f118 v
7 BEL23 ETB39 '55 771 G87 W103 g119 w
8 BS24 CAN40 (56 872 H88 X104 h120 x
9 TAB25 EM41 )57 973 I89 Y105 i121 y
10 LF26 SUB42 *58 :74 J90 Z106 j122 z
11 VT27 ESC43 +59 ;75 K91 [107 k123 {
12 FF28 FS44 ,60 <76 L92 \108 l124 |
13 CR29 GS45 -61 =77 M93 ]109 m125 }
14 SO30 RS46 .62 >78 N94 ^110 n126 ~
15 SI31 US47 /63 ?79 O95 _111 o127 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.
CallCostBecause
s.charCodeAt(i), s.codePointAt(i), String.fromCharCode(c)O(1), and O(k) to build k code unitsreading 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.

CallCostBecause
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.

CallCostBecause
`...${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.

Example 1: the file extension

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.

Run in Compiler
Example 2: cleaning the sign-up list

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.

Run in Compiler
Example 3: Bob's search box, fixed

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.

Run in Compiler

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, and path.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").pathname is "/books/strings/02". A router splits it on "/" into ['', 'books', 'strings', '02'] to decide which page to show.
  • nginx access logs. nginx's default combined log 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's replaceAll(",", ""): 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.

Brain teaser

Zara tries two methods on one word before she trusts them.

console.log("banana".replace("a", "o"));
console.log("banana".split("a"));

Predict both lines exactly, brackets and quotes included. How many pieces does the second line print, and why is the last one empty?

replace with a string changes only the first match. For split, mark every a in banana as a cut. Then write down what lies between the cuts, and after the last one.

Exercise 1Easy

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.

Run in Compiler
Exercise 2Easy

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.

Run in Compiler
Exercise 3Medium

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.

Run in Compiler
Exercise 4Hard

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?

Run in Compiler

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, includes and 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] or at, 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: includes for "is it there", indexOf for "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 it O(n) or O(n + k).
  • slice(start, end) leaves out the end and counts negatives from the end; substring swaps and reads negatives as 0; substr is legacy (Annex B).
  • indexOf gives a position or -1, so if (s.indexOf(x)) is wrong both ways: use includes for yes or no.
  • split cuts a string into an array and join glues it back; replace changes the first match and replaceAll every match.
  • charCodeAt and String.fromCharCode turn characters into codes and back (anchors 32, 48, 65, 97); < compares codes, localeCompare a 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