Strings
JavaScript keeps all text in strings. There’s no distinct type for a single character the way C has char — one letter is just a string of length one.
Internally, every string is stored as UTF-16, no matter how the surrounding page is encoded. UTF-16 means each string is a sequence of 16-bit numbers under the hood, and most characters you type map to exactly one of those units. A handful of characters (emoji, some rarer scripts) take two units, and that’s the source of a few surprises covered later and in the Unicode chapter. For everyday Latin text you can picture a string as a plain row of characters.
Quotes
Three characters can wrap a string: single quotes, double quotes, and backticks.
let single = 'single-quoted';
let double = "double-quoted";
let backticks = `backticks`;
Single and double quotes do the same job — pick whichever keeps the fewest escapes. Backticks give you two extra powers that the other two lack.
line breaks no
line breaks no
line breaks yes
The first power is interpolation. Inside a backtick string, wrap any expression in ${…} and JavaScript evaluates it, converts the result to a string, and drops it in place:
function total(a, b) {
return a + b;
}
alert(`4 + 5 = ${total(4, 5)}.`); // 4 + 5 = 9.
The expression can be anything: a variable, arithmetic, a function call, a property lookup. Whatever value comes back is coerced to text using the usual string conversion rules.
evaluate the expression, convert to string
Try it live. Type a name and a number of guests; the greeting is built with a single backtick string that interpolates both values and a small calculation:
The second power is real line breaks. A backtick string can span several lines and keep every break exactly as written:
let guestList = `Guests:
* Maya
* Raj
* Lena
`;
alert(guestList); // a list of guests, spread over multiple lines
The single- and double-quoted versions can’t do that. Break the line and the parser reports an error at the point where the string was left hanging:
let guestList = "Guests: // Error: Unexpected token ILLEGAL
* Maya";
Single and double quotes date back to the earliest days of the language, when multiline strings weren’t a concern. Backticks arrived much later, which is why they’re the more capable of the three.
Backticks have one more trick that’s easy to miss. You can place a function name right before the opening backtick — the syntax is func`string`. That function, func, runs automatically and receives the string pieces and the interpolated values as separate arguments, so it can process them however it likes. The feature is called “tagged templates.” You rarely need it, but it powers tools like styled-components and safe HTML builders. The details live on MDN under Template literals.
Special characters
Even single- and double-quoted strings can hold line breaks, using the newline character written as \n:
let guestList = "Guests:\n * Maya\n * Raj\n * Lena";
alert(guestList); // a multiline list of guests, same as above
To see that a plain \n and a real line break produce the same thing, compare the two forms directly:
let str1 = "Sea\nSky"; // two lines using a newline symbol
// two lines using an actual line break inside backticks
let str2 = `Sea
Sky`;
alert(str1 == str2); // true
\n is one of several backslash sequences. Here are the ones worth knowing:
| Character | Description |
|---|---|
\n |
Line break. |
\r |
Carriage return. Windows text files end each line with the pair \r\n; other systems use a lone \n. It’s historical baggage, and most Windows software accepts a bare \n anyway. |
\', \", \` |
A quote that matches the surrounding quotes, inserted as a literal character. |
\\ |
A single literal backslash. |
\t |
Tab. |
\b, \f, \v |
Backspace, form feed, vertical tab. Listed for completeness; they come from the typewriter era and you’ll never reach for them in practice. |
Every one of these starts with a backslash \, which is why the backslash is called the “escape character.”
Because the backslash is special, showing a literal one means writing two in a row:
alert( `The backslash: \\` ); // The backslash: \
The escaped quotes \', \", and \` let you place a quote inside a string wrapped in that same quote:
alert( 'We\'re open!' ); // We're open!
Without the backslash, that inner ' would close the string early, so it has to be escaped as \'.
You only need to escape the quote that matches the wrapper. A cleaner move is to switch the wrapper to a quote the text doesn’t use:
alert( "We're open!" ); // We're open!
There’s also a \u… notation for Unicode code points. It shows up rarely and gets its own treatment in the optional Unicode chapter.
String length
The length property holds the character count:
alert( `Go\n`.length ); // 3
\n is a single character, so the count is 3, not 4.
Accessing characters
To read the character at position pos, use square brackets [pos] or call str.at(pos). Positions start at zero:
let str = `Coral`;
// the first character
alert( str[0] ); // C
alert( str.at(0) ); // C
// the last character
alert( str[str.length - 1] ); // l
alert( str.at(-1) ); // l
at has a benefit the brackets don’t: a negative position counts back from the end. So .at(-1) is the last character, .at(-2) the one before it, and so on.
The brackets have no such convenience. Feed them a negative index and you get undefined:
let str = `Coral`;
alert( str[-2] ); // undefined
alert( str.at(-2) ); // a
You can also walk a string character by character with for..of:
for (let char of "Coral") {
alert(char); // C,o,r,a,l (char is "C", then "o", then "r", and so on)
}
Drag the slider to pick a position and watch both [pos] and .at(pos) respond. Negative positions show off .at’s trick — the brackets just return undefined:
Strings are immutable
A string, once made, can’t be changed. There’s no way to overwrite a character in place.
Here’s the attempt that proves it:
let str = 'Yo';
str[0] = 'y'; // error
alert( str[0] ); // doesn't work
The way around it is to build a whole new string and assign it back over the old variable:
let str = 'Yo';
str = 'y' + str[1]; // replace the whole string
alert( str ); // yo
This pattern — throw away the old string, keep the new one — shows up in most of the methods below. They never touch the original; they hand back a new string for you to use.
Changing the case
toLowerCase() and toUpperCase() flip the case of every letter:
alert( 'Keyboard'.toUpperCase() ); // KEYBOARD
alert( 'Keyboard'.toLowerCase() ); // keyboard
Want just one character in a different case? Index into the string first, then call the method on that single-character result:
alert( 'Keyboard'[0].toLowerCase() ); // 'k'
Searching for a substring
There’s more than one way to find a piece of text inside a string.
str.indexOf
The workhorse is str.indexOf(substr, pos).
It scans str for substr, beginning at position pos, and returns the position of the first match. If there’s no match at all, it returns -1.
let str = 'Bonfire beacon';
alert( str.indexOf('Bonfire') ); // 0, 'Bonfire' sits at the very start
alert( str.indexOf('bonfire') ); // -1, no match — the search is case-sensitive
alert( str.indexOf("on") ); // 1, "on" is found at position 1 (Bon...)
The optional second argument sets where the search starts. The first "on" is at position 1; to find the next one, start scanning from position 2:
let str = 'Bonfire beacon';
alert( str.indexOf('on', 2) ) // 12
To visit every occurrence, run indexOf in a loop, each time starting just past the previous hit:
let str = 'A bat, a cat, and a rat';
let target = 'at'; // the substring to look for
let pos = 0;
while (true) {
let foundPos = str.indexOf(target, pos);
if (foundPos == -1) break;
alert( `Found at ${foundPos}` );
pos = foundPos + 1; // resume from the character after this match
}
The same loop can be folded into a tighter form, doing the search and the test in one line:
let str = "A bat, a cat, and a rat";
let target = "at";
let pos = -1;
while ((pos = str.indexOf(target, pos + 1)) != -1) {
alert( pos );
}
One rough edge trips up nearly everyone. You can’t drop indexOf straight into an if like this:
let str = "Bonfire beacon";
if (str.indexOf("Bonfire")) {
alert("We found it"); // doesn't run!
}
The alert stays silent because str.indexOf("Bonfire") returns 0 — the match is at the start — and if treats 0 as falsy. A found-at-the-start result reads as “not found,” which is backwards.
The correct test compares against -1 explicitly:
let str = "Bonfire beacon";
if (str.indexOf("Bonfire") != -1) {
alert("We found it"); // runs now!
}
includes, startsWith, endsWith
When you only care whether the substring is present, not where, reach for the newer str.includes(substr, pos). It returns a plain true or false, which sidesteps the 0-versus--1 trap entirely:
alert( "Bonfire beacon".includes("Bonfire") ); // true
alert( "Coral".includes("kelp") ); // false
Its optional second argument is, again, the position to start from:
alert( "Bonfire".includes("on") ); // true
alert( "Bonfire".includes("on", 2) ); // false, no "on" from position 2 onward
And str.startsWith and str.endsWith do precisely what their names promise:
alert( "Bonfire".startsWith("Bon") ); // true, "Bonfire" starts with "Bon"
alert( "Bonfire".endsWith("ire") ); // true, "Bonfire" ends with "ire"
Here all three ideas come together: type a needle and see every match highlighted, the first position from indexOf, and the plain true/false from includes. Notice the search is case-sensitive:
Getting a substring
JavaScript hands you three methods for pulling a piece out of a string: slice, substring, and substr. They overlap heavily, and the differences are exactly the kind of thing that causes bugs, so it’s worth seeing them side by side.
Everything below works against this word and its positions:
str.slice(start [, end])Returns the piece from
startup to — but not including —end.let str = "afternoon"; alert( str.slice(0, 5) ); // 'after', characters 0 through 4 alert( str.slice(0, 1) ); // 'a', from 0 up to 1, so just the character at 0Leave out
endandsliceruns to the end of the string:let str = "afternoon"; alert( str.slice(2) ); // 'ternoon', from position 2 onwardstartandendcan be negative, in which case they count from the right end of the string:let str = "afternoon"; // start 4 from the right, end 1 from the right alert( str.slice(-4, -1) ); // 'noo'
str.substring(start [, end])Returns the piece between
startandend(not includingend).This is nearly identical to
slice, with one difference:substringletsstartbe greater thanend. When that happens it quietly swaps the two.let str = "afternoon"; // these two are the same for substring alert( str.substring(2, 6) ); // "tern" alert( str.substring(6, 2) ); // "tern" // ...but not for slice: alert( str.slice(2, 6) ); // "tern" (same) alert( str.slice(6, 2) ); // "" (empty string)Negative arguments aren’t supported the way they are in
slice—substringtreats any negative value as0.
str.substr(start [, length])Returns the piece starting at
start, running forlengthcharacters.This is the odd one out: the second argument is a count, not an end position.
let str = "afternoon"; alert( str.substr(2, 4) ); // 'tern', 4 characters starting at position 2startmay be negative, to count from the right:let str = "afternoon"; alert( str.substr(-4, 2) ); // 'no', 2 characters starting 4 from the endsubstrlives in Annex B of the language spec, the section for legacy browser features. In theory a non-browser engine could skip it. In practice it works everywhere.
The swap behavior is the sharpest edge between slice and substring, so it’s worth pinning down:
Here’s the whole comparison in one place:
| method | selects… | negatives |
|---|---|---|
slice(start, end) |
from start to end (not including end) |
allows negatives |
substring(start, end) |
between start and end (not including end) |
negative values mean 0 |
substr(start, length) |
from start, take length characters |
allows negative start |
Move the two sliders and compare all three at once. The same numbers feed slice, substring, and substr — watch where they agree and where they diverge (try a first slider larger than the second, or a negative first value):
Comparing strings
From the Comparisons chapter you know strings compare character by character, in what looks like alphabetical order. There are a couple of results that don’t match everyday expectations.
-
A lowercase letter always ranks above the same-position uppercase letter:
alert( 'a' > 'Z' ); // true -
Letters with diacritics land “out of order”:
alert( 'Übungen' > 'Zenit' ); // trueSort a list of words and this bites you: most people expect
Zenitto followÜbungen, not precede it.
The explanation is the UTF-16 encoding. Each character carries a numeric code, and strings are compared by those codes.
Two methods bridge characters and their codes:
str.codePointAt(pos)Returns the numeric code of the character at position
pos, as a decimal:// letters of different case carry different codes alert( "Q".codePointAt(0) ); // 81 alert( "q".codePointAt(0) ); // 113 alert( "q".codePointAt(0).toString(16) ); // 71 (hexadecimal, if you want it)Full reference: str.codePointAt(pos).
String.fromCodePoint(code)Builds a character from its numeric
code:alert( String.fromCodePoint(81) ); // Q alert( String.fromCodePoint(0x51) ); // Q (a hex code works too)Full reference: String.fromCodePoint(code).
Line up the codes from 65 to 220 — the Latin alphabet plus a bit extra — by turning each one into a character:
let str = '';
for (let i = 65; i <= 220; i++) {
str += String.fromCodePoint(i);
}
alert( str );
// Output:
// ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~
// ¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜ
Notice the layout: capitals come first, then a few punctuation characters, then the lowercase letters, with Ö sitting far down near the end.
Now a > Z makes sense. Comparison walks the numeric codes: the bigger code is the “bigger” character. The code for a is 97, the code for Z is 90, and 97 > 90.
- Every lowercase letter outranks every uppercase letter, because its code is higher.
- Letters like
Ösit apart from the core alphabet, with a code above everything fromatoz.
Correct comparisons
Ordering text “properly” is harder than it looks, because every language sorts its alphabet differently. The browser has to know the language to get it right.
Modern browsers implement the internationalization standard ECMA-402, which exists to handle exactly this.
Its string method is str.localeCompare(str2). It reports how str ranks against str2 under the active language’s rules:
- a negative number if
strshould come beforestr2, - a positive number if
strshould come afterstr2, 0if they’re considered equal.
alert( 'Übungen'.localeCompare('Zenit') ); // -1
So under locale rules, Übungen sorts before Zenit — the result people actually want. localeCompare also takes two more optional arguments, documented on MDN: one picks the language (it defaults to the environment, and letter order depends on it), and the other tunes rules like case sensitivity or whether "a" and "á" count as the same letter.
The difference is easy to feel with a real list. Edit the words if you like, then sort the same array two ways — by raw character code (<) and by localeCompare. Watch the accented and lowercase entries jump around:
Summary
- There are three quote styles. Backticks alone let a string span multiple lines and embed expressions with
${…}. - Special characters, such as the line break
\n, are written with a leading backslash. - To read one character, use
[]orat. - To pull out a substring, use
slice(orsubstring). - To change case, use
toLowerCaseandtoUpperCase. - To search, use
indexOf, orincludes/startsWith/endsWithfor simple presence checks. - To compare the way a reader expects, use
localeCompare; otherwise strings compare by character code.
A few more methods worth keeping in your back pocket:
str.trim()removes whitespace from both ends of the string.str.repeat(n)gluesncopies of the string together.- …and the rest are catalogued in the String reference.
Strings can also search and replace using regular expressions. That’s a large topic with its own section: regular expressions.
Keep in mind that strings rest on Unicode, which is where the comparison quirks come from. The Unicode, String internals chapter goes deeper into how that encoding works and how to handle the characters that span two code units.