Key Takeaways
- Prefer primitive strings over objects created with
new String(). - Strings are immutable: methods such as
replace()return a result rather than changing the original text. - ES6 added template literals and methods including
startsWith(),endsWith(),includes(), andrepeat(); many other string methods are older. lengthcounts UTF-16 code units, not necessarily the characters a reader sees.- Template interpolation constructs text. It does not automatically make that text safe for HTML or SQL.
JavaScript strings are primitive values used to represent text. JavaScript string methods let you search, extract, compare, and transform that text, while ES6 template literals support embedded expressions and multiline strings.
This guide covers the original ES6 additions alongside everyday methods you still need. For the broader language background, start with our JavaScript ES6 introduction.
Defining Strings
Single and double quotes both create primitive strings. Backticks create template literals, which we'll use for interpolation later. Run each example block independently.
const firstName = "John";
const lastName = 'Doe';
const myLiteral = `this is a literal`;Primitive strings versus String objects
JavaScript automatically boxes a primitive stringβtemporarily treating it as a wrapper objectβfor method access. You don't need to create that wrapper yourself: String(value) converts a value to a primitive string, whereas new String(value) creates an object.
const primitive = "John";
const wrapped = new String("John"); // Demonstration only; avoid in new code.
console.log(typeof primitive); // "string"
console.log(typeof wrapped); // "object"
console.log(primitive === wrapped); // false
console.log(String(30)); // "30"
console.log(Boolean("")); // false
console.log(Boolean(new String(""))); // trueThe empty wrapper is truthy because it's an object, even though an empty primitive string is falsy. Prefer primitives to avoid these type and comparison surprises.
Strings are immutable
Immutable means the existing value cannot be changed. Calling a transformation method without using its return value leaves your original string untouched.
let text = "hello world";
text.replace("world", "JavaScript");
console.log(text); // "hello world"
text = text.replace("world", "JavaScript");
console.log(text); // "hello JavaScript"Prefer const when you won't reassign a variable and let when you will. Unlike var, both support block scope; the declaration doesn't change string immutability. Our guide to JavaScript variable declarations explains the distinction.
Which String Features Came from ES6?
- Before ES6: methods including
charAt(),indexOf(),slice(),split(),trim(), andvalueOf(). - ES6, also called ECMAScript 2015: template literals,
startsWith(),endsWith(),includes(),repeat(),codePointAt(),String.fromCodePoint(), andString.raw(). - Later additions covered here:
padStart(),padEnd(),matchAll(), andreplaceAll(). These aren't ES6 features.
This is a practical survey, not the complete API. Use the MDN String reference when you need the full method list.
JavaScript String Methods: Everyday Operations
length: count code units, not visible characters
length is a property, not a method. It counts UTF-16 code units, including those used for spaces. A code unit is a 16-bit unit of string data; some Unicode characters, including this emoji, require two.
| Text unit | Useful API | What it represents |
|---|---|---|
| UTF-16 code unit | length, indexing, charCodeAt() | The units used for string offsets. |
| Unicode code point | String iteration, codePointAt() | A Unicode numeric value; some require two code units. |
| Grapheme cluster | Intl.Segmenter | A user-perceived character, potentially containing multiple code points. |
console.log("hello".length); // 5
console.log("π".length); // 2
console.log([..."π"].length); // 1
const combined = "e\u0301"; // e followed by a combining acute accent
console.log(combined.length); // 2
console.log([...combined].length); // 2
// Requires Intl.Segmenter support.
const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
console.log([...segmenter.segment(combined)].length); // 1
console.log([...segmenter.segment("π")].length); // 1Spreading a string counts code points, not grapheme clusters. Use Intl.Segmenter when your feature needs user-perceived characters, and check support in your target environment.
charAt(): read one code unit
Indexes start at zero. charAt() returns a one-code-unit string, or an empty string for an out-of-range index; it can return only half of an emoji's surrogate pair.
console.log("hello".charAt(2)); // "l"
console.log("hello".charAt(99)); // ""charCodeAt() versus codePointAt()
charCodeAt() returns a UTF-16 code-unit value. codePointAt() reads the complete code point when its index points to the beginning of a surrogate pair, but its index still uses code-unit offsets.
console.log("hello".charCodeAt(2)); // 108
console.log("π".charCodeAt(0)); // 55357: the leading surrogate
console.log("π".codePointAt(0)); // 128512: U+1F600concat(): combine strings
concat() returns the strings joined together. It doesn't insert separators for you.
const firstName = "John";
const lastName = " Doe";
console.log(firstName.concat(lastName)); // "John Doe"indexOf() and lastIndexOf(): locate text
indexOf() returns the first matching position; lastIndexOf() returns the last. Both return -1 when the substring isn't found.
console.log("John".indexOf("o")); // 1
console.log("John".indexOf("z")); // -1
console.log("banana".lastIndexOf("a")); // 5
console.log("banana".lastIndexOf("z")); // -1Don't use the returned index directly as a condition: zero is a valid match position. Compare against -1, or use includes() for a yes/no answer.
localeCompare(): compare using language rules
localeCompare() returns a negative number when the left string sorts before the argument, a positive number when it sorts after, or zero when they're equivalent under the comparison rules. It does not promise exactly -1 or 1; locale and options affect ordering.
const text = "hello world";
const options = { sensitivity: "variant" };
console.log(text.localeCompare("goodbye", "en", options) > 0); // true
console.log(text.localeCompare("zed", "en", options) < 0); // true
console.log(text.localeCompare("hello world", "en", options)); // 0match() and matchAll(): extract regex matches
A regular expression, or regex, describes a text pattern. With match(), the global g flag changes the result: without it you get the first match plus capture groups and metadata; with it you get all full matched substrings, without capture groups. No match returns null.
const fruit = "banana";
console.log(fruit.match(/a/g)); // ["a", "a", "a"]
console.log(fruit.match(/z/)); // null
const first = fruit.match(/(a)/);
if (first !== null) {
console.log(first[0], first[1], first.index); // "a", "a", 1
}Use matchAll() when you need capture groups for every occurrence. It returns an iterator of match arrays; a regex argument must have g, otherwise it throws TypeError.
for (const match of "id=12 id=34".matchAll(/id=(\d+)/g)) {
console.log(match[1]); // "12", then "34"
}replace() and replaceAll(): transform matched text
replace() replaces the first occurrence of a string search value, or the matches selected by a regex. A global regex replaces every match. replaceAll() replaces every occurrence of a string search value; a regex argument must be global. Neither method changes the original string.
const text = "hello world";
console.log(text.replace(/l/g, "z")); // "hezzo worzd"
console.log(text.replace(/l/, "z")); // "hezlo world"
console.log(text.replace("l", "z")); // "hezlo world"
console.log(text.replaceAll("l", "z")); // "hezzo worzd"search(): locate a regex match
search() returns the first matching index, or -1 when there is no match.
console.log("hello world".search(/world/)); // 6
console.log("hello world".search(/zed/)); // -1slice() versus substring(): extract a range
Both methods include the start index and exclude the end index. slice() counts negative indexes from the end; substring() treats negative arguments as zero and swaps start and end when necessary. Prefer slice() as your usual range-extraction method.
const text = "hello world";
console.log(text.slice(0, 2)); // "he"
console.log(text.slice(2, 4)); // "ll"
console.log(text.substring(2, 3)); // "l"
console.log(text.slice(-5)); // "world"
console.log(text.substring(-5)); // "hello world"
console.log(text.slice(4, 2)); // ""
console.log(text.substring(4, 2)); // "ll"These ranges use code-unit offsets, so arbitrary boundaries can split a surrogate pair. See the substring comparison notes for additional edge cases.
Legacy substr(): replace carefully
substr() is a legacy, deprecated API. Its second argument is a length, not an end index, so replacing its name with slice isn't a safe general migration. These examples show modern equivalents for common calls.
const text = "hello world";
console.log(text.slice(-1)); // "d"; replaces text.substr(-1)
console.log(text.slice(0, 5)); // "hello"; replaces text.substr(0, 5)
console.log(text.slice(2, 5)); // "llo"; replaces text.substr(2, 3)split(): turn text into an array
split(separator, limit) returns substrings separated by the supplied delimiter. The optional limit caps the number of returned entries and discards the remaining text.
const text = "hello world";
console.log(text.split(" ")); // ["hello", "world"]
console.log(text.split("l")); // ["he", "", "o wor", "d"]
console.log(text.split("l", 2)); // ["he", ""]split("") separates code units, so it isn't a Unicode-aware character splitter. Once you've split your text, our JavaScript array methods tutorial covers working with the resulting array.
toLowerCase() and toUpperCase(): change case
These methods return lowercase or uppercase text without modifying the original value.
const text = "Hello World";
console.log(text.toLowerCase()); // "hello world"
console.log(text.toUpperCase()); // "HELLO WORLD"valueOf(): obtain the primitive value
valueOf() returns the primitive string represented by a string or wrapper object. You rarely need it when you use primitives throughout your code.
console.log("hello world".valueOf()); // "hello world"
console.log(new String("hello world").valueOf()); // "hello world"trim(), padStart(), and padEnd(): clean and format text
trim() removes leading and trailing whitespace, not internal spaces. The padding methods add text at the beginning or end until the requested total length is reached; they don't truncate an already longer string.
console.log(" hello world\n".trim()); // "hello world"
console.log("7".padStart(3, "0")); // "007"
console.log("7".padEnd(3, ".")); // "7.."
console.log("hello".padStart(3, "0")); // "hello"ES6 String Checks and Repetition
The three checking methods below are case-sensitive and accept strings rather than regex patterns. Their positions use code-unit offsets.
startsWith(): check a prefix
The optional position says where the prefix must begin; it defaults to zero.
const text = "hello world";
console.log(text.startsWith("hel")); // true
console.log(text.startsWith("el")); // false
console.log(text.startsWith("el", 1)); // trueendsWith(): check a suffix
The optional end position limits the portion considered, as though you were checking that prefix of the string. It defaults to the string's length.
const text = "hello world";
console.log(text.endsWith("world")); // true
console.log(text.endsWith("world", 11)); // true
console.log(text.endsWith("world", 10)); // falseincludes(): check whether text exists
The optional position specifies where searching begins; it defaults to zero.
const text = "hello world";
console.log(text.includes("world")); // true
console.log(text.includes("world", 7)); // falserepeat(): repeat text
repeat(count) returns consecutive copies without adding separators. Validate user-controlled counts before allocating large strings.
console.log("hello world".repeat(3));
// "hello worldhello worldhello world"Template Literals
Template literals use backticks instead of single or double quotes. Their main advantages are interpolation and literal line breaks.
String interpolation with expressions
Inside an ordinary, untagged template literal, ${expression} evaluates a JavaScript expression and converts its value to text. Text outside placeholders remains literal text.
const myAge = 30;
const myName = "John";
const statement = `Hello, my name is ${myName} and I am ${myAge} years old.`;
console.log(statement);
// Hello, my name is John and I am 30 years old.
console.log(`Next year I'll be ${myAge + 1}.`); // "Next year I'll be 31."
console.log("Hello, ${myName}"); // "Hello, ${myName}"Multiline strings preserve their line breaks
Line breaks and indentation inside the backticks become part of the resulting string. They're not just source-code formatting.
const myString = `This is a string that
spans multiple lines
stored as myString.`;
console.log(myString);Tagged templates pass the parts to a function
A tag is a JavaScript function placed immediately before a template literal. It receives an array of literal sections followed by the interpolated values, and its return value becomes the resultβeven if that result isn't a string.
// This demonstration tag handles one interpolation.
function emphasize(parts, value) {
return parts[0] + String(value).toUpperCase() + parts[1];
}
console.log(emphasize`Hello, ${"John"}!`); // "Hello, JOHN!"Other ES6 String Methods
String.fromCodePoint(): construct text from Unicode values
String.fromCodePoint() accepts Unicode code points, including values above 0xFFFF. Unlike String.fromCharCode(), you don't need to supply the surrogate pair yourself for those characters.
console.log(String.fromCodePoint(102, 110)); // "fn"
console.log(String.fromCodePoint(0x1F600)); // "π"String.raw: preserve literal backslashes
String.raw is a built-in template tag. In the literal sections, it preserves escape sequences such as \n as backslash-containing text; interpolation still works. This is useful for paths and other backslash-heavy text.
const myString = String.raw`hello \world`;
console.log(myString); // hello \world
console.log(String.raw`line\n${2 + 3}`); // line\n5Common Mistakes: Interpolation Is Not Escaping
Backticks make string construction convenient, not automatically secure. Neither ordinary interpolation nor String.raw sanitizes untrusted values for their destination.
- HTML: For plain text in an ordinary DOM element, use
textContentinstead of interpolating untrusted values intoinnerHTML. When you actually need HTML, follow OWASP's context-specific XSS prevention guidance. - SQL: Pass values through your database driver's parameterized-query API instead of inserting them into SQL strings. A template tag is safe only if its implementation actually provides the required protection; backticks alone don't. See OWASP's SQL injection prevention guidance.
For everyday work, start with includes() for existence checks, indexOf() for positions, slice() for ranges, and template literals for readable text construction. Decide separately whether your feature needs code-unit, code-point, or grapheme-cluster boundaries.
FAQ
What is the difference between a JavaScript string and a String object?
A string literal creates a primitive value; new String() creates a wrapper object. JavaScript provides method access on primitives automatically, so prefer primitives to avoid surprising type, equality, and truthiness behavior.
How do template literals work in JavaScript?
Template literals use backticks and embed expressions with ${expression}. Untagged templates convert those expression values to text and preserve literal line breaks; tagged templates pass the literal sections and expression values to a function.
What is the difference between slice() and substring()?
Both extract text using an inclusive start and exclusive end. slice() supports negative indexes relative to the end, while substring() treats negative arguments as zero and swaps reversed start and end arguments.
Why does 'π'.length return 2 in JavaScript?
The length property counts UTF-16 code units, and this emoji requires two code units. String iteration counts it as one code point, while Intl.Segmenter can count user-perceived characters that contain multiple code points.
What is the difference between replace() and replaceAll()?
With a string search value, replace() replaces only the first occurrence and replaceAll() replaces every occurrence. replace() can also replace every match using a global regex; replaceAll() requires the global flag when its search value is a regex.
Further Reading
- MDN String reference β the complete property and method catalog.
- ECMAScript 2015 specification β the standard behind the ES6 features.
- MDN template literals β interpolation, multiline text, and tags.
- MDN Intl.Segmenter β grapheme, word, and sentence segmentation.
- MDN matchAll() β iterating matches with capture groups.
- MDN replaceAll() β replacement behavior and regex requirements.
- MDN String.raw() β raw literal sections and substitutions.
- OWASP XSS prevention cheat sheet β safely placing untrusted text into web pages.
