JAVASCRIPT / STRINGS AND TEMPLATE LITERALS
Indexing, length, and character access
Read a string's length, pull characters out with brackets, charAt and at(), and explain why .length counts UTF-16 code units rather than visible characters.
What you will learn
- Get the last character with str[str.length - 1] or the shorter str.at(-1)
- Know that str[9] is undefined while str.charAt(9) is an empty string
- Loop indices 0 to length - 1 to build a new string, since strings never change in place
- Use [...str] or for...of when text may contain emoji so surrogate pairs stay whole
Understanding Indexing, length, and character access
A JavaScript string is a fixed sequence of UTF-16 code units, and every slot in that sequence has a number that starts at 0. That is why length is a data property and not a method: the count is stored on the string itself, so no call is needed, and the highest usable index is always length - 1. Slots are read-only. Reading str[2] hands you back a brand new one-unit string, and nothing you write to str[2] can change the original, because the string value itself can never be edited.
There are three ways to read a slot, and they disagree about bad input. Bracket access is ordinary property lookup: the number is turned into a string key, and a key that does not exist gives undefined, exactly like any missing property, which is also why str[-1] is undefined instead of counting backwards. charAt is specified to return an empty string when the index is out of range, so it hides mistakes inside a value that still behaves like a string. at() arrived in ES2022 and is the only one that accepts negative offsets, counting back from the end and returning undefined when you overshoot.
length counts code units, not the characters a reader would point at. Latin letters, Greek, Cyrillic and most common CJK live in the Basic Multilingual Plane and cost one unit each, but emoji, rare CJK and musical symbols sit above U+FFFF and are stored as a surrogate pair, which is two units. That means an index can land in the middle of one character: "🙂".length is 2, and "🙂"[0] is a lone half that no font can draw. String iteration is defined over code points instead of units, so for...of and [...str] step over such a pair as a single element.
const city = "Kandy";
console.log(city.length); // a property, no parentheses
console.log(city[0]); // first slot
console.log(city[city.length - 1]); // last slot
console.log(city.at(-1)); // same slot, counted from the end
console.log(city[5]); // one past the end
console.log(`charAt(5) gives "${city.charAt(5)}"`);
console.log(typeof city[0]);A string is a read-only, zero-based sequence of UTF-16 code units, so indexing reads a unit and never a guaranteed whole character.
Worked examples
Walking every index
Shows why the loop condition is i < length and what happens at the first index past the end.
const word = "dune";
for (let i = 0; i < word.length; i++) {
console.log(i, word[i]);
}
console.log("index 4 gives", word[4]);Example explained
Line 1word.length is 4, so the condition i < word.length stops the loop after i is 3.
Line 2The last valid index is length - 1, which is why i <= word.length would run one time too many.
Line 3word[4] is a property that does not exist on the string, so the lookup produces undefined rather than an error.
Reading is allowed, writing is not
Demonstrates that assigning to an index is rejected, and that reversing means building a new string from old indices.
const word = "stressed";
function tryToEdit(s) {
"use strict";
s[0] = "S";
}
try {
tryToEdit(word);
} catch (err) {
console.log(err.name);
}
let reversed = "";
for (let i = word.length - 1; i >= 0; i--) {
reversed += word[i];
}
console.log(word, reversed);Example explained
Line 1The index properties of a string are non-writable, so under strict mode the assignment throws a TypeError instead of doing nothing.
Line 2Outside strict mode the same line fails silently, which is why the demo pins the mode inside the function.
Line 3The loop starts at length - 1 and stops at 0, reading each unit and appending it to a separate string.
Line 4word is untouched afterwards: reversed is a new value, not a rearranged copy of the original.
When one character takes two indices
Shows the gap between length, code units and code points for a string containing an emoji.
const text = "hi🙂";
console.log(text.length);
console.log(text.charCodeAt(2), text.charCodeAt(3));
console.log(text.codePointAt(2));
console.log([...text].length, [...text][2]);Example explained
Line 1length is 4 because the emoji occupies two code units, indices 2 and 3.
Line 2charCodeAt returns each surrogate half on its own: 55357 and 56898 mean nothing in isolation.
Line 3codePointAt(2) notices a surrogate pair and combines both halves into 128578, which is U+1F642.
Line 4Spreading uses the string iterator, which advances by code point, so the array has 3 entries and index 2 is the whole emoji.
Important notes
str[0] and str.charAt(0) both give a one-unit string; JavaScript has no character type, so typeof str[0] is always "string" and you compare it with "a", not 'a' as a distinct kind of value.
Counting by code point still is not counting what a reader calls a character: "e" followed by the combining accent \u0301 renders as é but has length 2 and two code points, and only Intl.Segmenter with granularity "grapheme" groups it as one.
Common mistakes
Writing str.length() — length is a data property, so calling it throws TypeError: str.length is not a function.
Looping with i <= str.length, which reads one slot past the end; the resulting undefined is concatenated as the literal text "undefined" instead of raising an error.
Doing str[0] = "X" and assuming the string changed: it fails silently in a plain script but throws a TypeError inside a module or a class, so the bug only appears after the file is loaded as a module.
Try it yourself
Change, predict, then run
In a browser console, set const s = "JavaScript" and log the first character, the middle one at Math.floor(s.length / 2), and the last one without typing any literal index. Then try s[0] = "j" and print s again to confirm it is unchanged.
Open the JavaScript workspaceCheck your understanding
A field holds the string "OK🙂". Your code shows text[text.length - 1] as the last character, but the page renders a broken box. What explains it?
- at(-1) is required here, because bracket notation cannot read the final position of a string.
- Emoji are stored outside the string body, so length does not count them.
- length counts UTF-16 code units, so the final index holds only the low half of the emoji's surrogate pair.
- The string is immutable, so reading an index returns a copy that drops non-Latin characters.
Show answer
length is 4 here (O, K, and the two halves of U+1F642), so index 3 is a lone low surrogate that no font can render; [...text].at(-1) or a code-point-aware iteration returns the whole emoji. Option 1 is tempting because at(-1) is the usual advice for the last position, but at() indexes the same code units and returns exactly the same broken half.