Tutorials

JavaScript ES6 Strings

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(), and repeat(); many other string methods are older.
  • length counts 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(""))); // true

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

Advertisement

Which String Features Came from ES6?

  • Before ES6: methods including charAt(), indexOf(), slice(), split(), trim(), and valueOf().
  • ES6, also called ECMAScript 2015: template literals, startsWith(), endsWith(), includes(), repeat(), codePointAt(), String.fromCodePoint(), and String.raw().
  • Later additions covered here: padStart(), padEnd(), matchAll(), and replaceAll(). 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 unitUseful APIWhat it represents
UTF-16 code unitlength, indexing, charCodeAt()The units used for string offsets.
Unicode code pointString iteration, codePointAt()A Unicode numeric value; some require two code units.
Grapheme clusterIntl.SegmenterA 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);     // 1

Spreading 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+1F600

concat(): 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")); // -1

Don'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)); // 0

match() 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/));   // -1

slice() 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)); // true

endsWith(): 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)); // false

includes(): 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)); // false

repeat(): 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\n5

Common 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 textContent instead of interpolating untrusted values into innerHTML. 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

Advertisement

Written by Sam Erickson

Pen name of a software engineer with 20+ years in the industry, from Fortune 500 companies to startups. Newer articles are researched and drafted with AI tools I direct and fact-checked against their sources. Author of Mastering the Tech Interview. More about me

Advertisement