10. Strings up close
Until now a string has been a lump of text. Look closer and it is a row of numbered characters, and every character is secretly a number. That is all a scanner needs.
- Read any character of a string by its index, and explain why strings can't be changed in place.
- Convert between characters and their char codes, and use codes to test for digits and letters.
- Cut strings apart and join them with slice, split and join, and search them with includes and startsWith.
- Build strings with template literals and piece by piece in a loop.
Text, as a computer sees it
Here’s a line of a program, as a string:
const src = 'width = 42;';
To you it obviously says “set width to 42”. To JavaScript it’s just eleven
characters in a row. In Part III you’ll write a scanner, the first
stage of an interpreter, whose whole job is to look at text like this and
find the words in it: the name width, the =, the number 42, the
;. To do that, you need to get inside a string: read it one character
at a time, ask what kind of character each one is, and cut out pieces.
That’s this chapter.
A string is a row of letter tiles, like in a word game. Each tile has a position you can point at. Turn a tile over and there’s a number printed on the back: the computer stores the number, and the letter is just how it’s drawn on screen.
A row of characters
Strings index exactly like arrays:
const s = 'cat 42';
console.log(s.length); // 6
console.log(s[0]); // c
console.log(s[4]); // 4 (a string!)
console.log(s[99]); // undefined
s[4] is the one-character string '4', not the number 4. Asking for
an index past the end gives undefined, as with arrays.
There’s one big difference from arrays: you can’t change a string.
let t = 'cat';
t[0] = 'b';
console.log(t); // cat
The assignment simply doesn’t work[1]. Strings are
immutable: once made, a string never changes[3].
You can still make t hold a different string (t = 'bat'), but the
string 'cat' itself stays exactly as it was.
str[i] reads the character at index i, as a string of length 1.
str.length is the number of characters. Strings are immutable:
every operation that seems to change one actually makes a new string.
Characters are numbers
charCodeAt(i) turns the tile over and shows the number on the back.
String.fromCharCode(n) goes the other way:
console.log('A'.charCodeAt(0)); // 65
console.log('a'.charCodeAt(0)); // 97
console.log('0'.charCodeAt(0)); // 48
console.log(String.fromCharCode(104, 105));
// hi
The useful part is the order. The digits '0' to '9' have the
codes 48 to 57, one after another. 'A' to 'Z' are 65 to 90, and
'a' to 'z' are 97 to 122. That turns “is this a digit?” into a
comparison:
function isDigit(c) {
return c >= 48 && c <= 57;
}
const c = '7'.charCodeAt(0); // 55
console.log(isDigit(c)); // true
console.log(c - 48); // 7
&& means “and”: the result is true only if both sides are true. The
last line shows a second trick: subtract 48 (the code of '0') and a
digit character’s code becomes the digit’s value.
Every character is stored as a number, its char code.
str.charCodeAt(i) returns the code at index i, a whole number from
0 to 65535[2]. String.fromCharCode(n) makes
the character with code n. Digits and letters have consecutive codes,
so a range test like c >= 48 && c <= 57 identifies them.
Cutting a string into pieces
Now put the two ideas together. Here’s the heart of a scanner: a finger
i moves along the string, and whenever it finds a run of digits, it
cuts the whole run out as one piece.
const s = '42+7';const parts = [];let i = 0;while (i < s.length) {const start = i;if (isDigit(s.charCodeAt(i))) {while (isDigit(s.charCodeAt(i))) i++;} else {i++;}parts.push(s.slice(start, i));}
The cutting is done by slice(start, end). It copies the characters
from start up to, but not including, end, and returns them as a
new string. Leaving end out means “to the end”, and a negative number
counts back from the end.
Strings come with many more ready-made methods. These are the ones you’ll use most:
const s = 'let x = 42;';
s.slice(0, 3); // 'let'
s.slice(-3); // '42;'
s.includes('x'); // true
s.startsWith('let'); // true
s.split(' ');
// [ 'let', 'x', '=', '42;' ]
['a', 'b'].join('-'); // 'a-b'
split cuts at every copy of a separator and gives back an array.
join is its opposite: it’s an array method that glues the slots
together with a separator between them.
The string methods you’ll use most: slice(start, end) copies a
piece (end not included); includes(x) and startsWith(x) answer
true or false; split(sep) cuts a string into an array of strings; and
the array method join(sep) glues an array back into one string. None
of them change the original string: they return new values.
const s = 'let x = 42;';
console.log(s.length, s[4]);
console.log(s.charCodeAt(8));
console.log(s.slice(8, 10));
console.log(s.includes('42'));
console.log(s.startsWith('var'));
const words = s.split(' ');
console.log(words);
console.log(words.join('_'));Building strings
Gluing strings with + gets messy fast. Template literals are
strings written between backticks, where ${...} drops in the value of
any expression[4]:
const name = 'x';
const v = 42;
const a = 'Set ' + name + ' to ' + v;
const b = `Set ${name} to ${v}`;
console.log(a === b); // true
And when you don’t know in advance how long the string will be, build it
piece by piece: start with an empty string '' and add to it in a loop.
function bar(n) {
let out = '';
for (let i = 0; i < n; i++) {
out += '#';
}
return out;
}
console.log(`[${bar(3)}]`); // [###]
out += '#' is short for out = out + '#'. Because strings are
immutable, each step makes a new, longer string and points out at it.
A common alternative is to push the pieces into an array and join
them once at the end.
A template literal is a string in backticks, where ${expr} is
replaced by the value of expr. Building a string means starting
from '' and adding pieces, with += or by collecting them in an array
and calling join.
A JavaScript string is a sequence of 16-bit UTF-16 code units, and
charCodeAt returns one of them[1]. Most characters
you’ll meet fit in one unit, but some, like many emoji, take two, so
'😀'.length is 2. Our scanner in Part III only cares about plain
letters, digits and symbols, so one unit per character is all it needs.
In Part IV, the virtual machine stores strings as objects of its own,
and keeps exactly one copy of each distinct string. Then checking
whether two names are equal becomes a single identity check, the ===
on references from chapter 4.
Your turn
The core exercise is a piece of every scanner: turning the digit characters of a number into an actual number.
Write toNumber(s) that turns a string of digits, like '472', into the number 472. Don't use Number or parseInt: work it out from the char codes, the way a scanner does. You may assume s contains only the digits 0 to 9. The empty string gives 0.
function toNumber(s) {
// your code here
}
console.log(toNumber('472'));Write shift(s, k) that moves every lowercase letter k places along the alphabet, wrapping around from z back to a. Everything that isn’t a lowercase letter stays as it is. So shift('abc', 1) is 'bcd' and shift('xyz', 3) is 'abc'. k is between 0 and 25.
function shift(s, k) {
// your code here
}
console.log(shift('hal', 1));What you learned
- A string is a row of characters:
str[i]reads one by its index, andstr.lengthcounts them. Strings are immutable. - Every character has a char code.
charCodeAtandfromCharCodeconvert between them, and code ranges identify digits and letters. - String methods
slice,includes,startsWithandsplit, with the array methodjoin, cut, search and glue strings. They return new values and never change the original. - Template literals drop values into a string with
${...}, and string building grows a result piece by piece.
Next: what to do when something goes wrong, such as a character your scanner doesn’t recognize.
Check your understanding
0/6- 1.
What is
'hello'.lengthand what is'hello'[5]? - 2.
'a'.charCodeAt(0)is 97. What isString.fromCharCode(99)? - 3.
What is
'let x = 1'.slice(4, 5)? - 4.
What does this print?
const s = 'abc'; s.toUpperCase(); console.log(s); - 5.
What is
'a,b,c'.split(',').join(' + ')? - 6.
A scanner holds
c = src.charCodeAt(i). Which test is true exactly when that character is a digit?
References
- [1] MDN contributors. String. MDN Web Docs. developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String (checked 2026-10-31)
- [2] MDN contributors. String.prototype.charCodeAt(). MDN Web Docs. developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/charCodeAt (checked 2026-10-31)
- [3] MDN contributors. Primitive (Glossary). MDN Web Docs. developer.mozilla.org/en-US/docs/Glossary/Primitive (checked 2026-10-31)
- [4] MDN contributors. Template literals (Template strings). MDN Web Docs. developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals (checked 2026-10-31)