AI InterviewSearch…Search problems, topics, or pages…Ctrl KGet started
Skip to content

10 Strings, Unicode, regular expressions and parsing / Lesson 2 of 5

The string toolbox, template literals, tagged templates and Intl formatting

12 min · +10 XP · 2 exercises · 5 questions

Read the text12 min
Exercises0/2
Quiz5 q

Quick answer

What is a tagged template in JavaScript?

A tagged template is a template literal with a function name placed directly before it. JavaScript calls that function with the literal's string parts as an array and the interpolated values as separate arguments, before any concatenation, so the function decides what interpolation means: escaping HTML, turning values into SQL parameters, or building CSS. String.raw is a built-in tag that returns the raw text.

Most string work is a dozen methods used well: find, slice, split, join, trim, pad, replace. The rest is formatting — turning numbers, dates and lists into text that is right for a reader — which is where hand-written code goes wrong in every locale but the author's, and where Intl gets it right for free. This lesson covers the methods with their exact semantics (slice versus substring, indexOf versus includes, split corner cases), template literals and the tagged-template mechanism that lets a function process a literal (the basis of html, sql and css tags), and the Intl formatters for numbers, currencies, dates and lists.

The methods, precisely

s.slice(2, 5); s.slice(-3);               // [start, end) — negatives count from the end; the one to use
s.substring(2, 5);                        // swaps arguments if start > end; treats negatives as 0 — legacy
s.indexOf("x"); s.lastIndexOf("x"); s.includes("x"); s.startsWith("x", from); s.endsWith("x", to);
s.at(-1);                                 // last code unit (Node 16.6+)
s.split(",");  s.split(",", 2);  s.split("");  s.split(/\s+/);
"a,b,".split(",");                        // ["a", "b", ""] — a trailing separator yields an empty last element
"".split(",");                            // [""] — not []
s.trim(); s.trimStart(); s.trimEnd();     // whitespace incl. Unicode spaces and line terminators
s.padStart(8, "0"); s.padEnd(20);         // to a target LENGTH (code units), truncating the pad if needed
s.repeat(3);                              // RangeError for negative or Infinity
s.replace("a", "b");                      // first occurrence only for a string pattern; $& $1 $<name> in the replacement
s.replaceAll("a", "b");                   // all (Node 15+); with a regex it must have the g flag
s.toUpperCase(); s.toLowerCase(); s.localeCompare(t); s.normalize();
s.concat(t) === s + t;  s.charAt(i) === s[i] (but "" instead of undefined out of range)

split with a regex containing a capture group includes the captures in the result — "a1b2c".split(/(\d)/) is ["a", "1", "b", "2", "c"], occasionally useful. replace with a string pattern and a string replacement interprets $ sequences ($$ for a literal dollar) — pass a function (() => text) when the replacement is user data.

Template literals

const line = `${user.name} has ${items.length} item${items.length === 1 ? "" : "s"}`;
const multi = `first line
second line`;                                            // real newlines preserved
`${1 + 1}`;  `${obj}`  /* calls toString */;  `${[1, 2]}` /* "1,2" */;  `${null}` /* "null" */

Interpolation calls String(value) (via the ToPrimitive rules of module 5): objects print [object Object] unless they define toString, arrays join with commas, undefined prints as undefined. For anything shown to users, format explicitly — an interpolated Date prints its long default form; an interpolated number prints without separators.

Tagged templates

A function placed before a template literal receives the literal parts and the values separately, before any concatenation:

function html(strings, ...values) {
  return strings.reduce((out, str, i) => out + str + (i < values.length ? escapeHtml(values[i]) : ""), "");
}
const safe = html`<p>${userInput}</p>`;      // strings = ["<p>", "</p>"], values = [userInput] — escaped automatically
String.raw`C:\new\table`;                    // "C:\\new\\table" — the raw text; strings.raw holds it in any tag

The tag decides what interpolation means: escaping (html), parameterising (sql\… WHERE id = ${id}\` → placeholders + parameters, no injection), styling (css, styled.div), localisation, GraphQL parsing. strings has a .raw property with the unescaped source text. Because the literal parts are known statically, a tag can cache parsing per call site — strings` is the same frozen array object on every evaluation of the same literal.

Intl: numbers, currency, dates, lists

new Intl.NumberFormat("en-US").format(1234567.891);                              // "1,234,567.891"
new Intl.NumberFormat("de-DE").format(1234567.891);                              // "1.234.567,891"
new Intl.NumberFormat("en-US", { style: "currency", currency: "EUR" }).format(9.5);   // "€9.50"
new Intl.NumberFormat("en", { notation: "compact" }).format(1_500_000);          // "1.5M"
new Intl.NumberFormat("en", { style: "percent", maximumFractionDigits: 1 }).format(0.1234);   // "12.3%"
new Intl.NumberFormat("en", { style: "unit", unit: "kilometer-per-hour" }).format(88);         // "88 km/h"
new Intl.DateTimeFormat("en-GB", { dateStyle: "long", timeZone: "UTC" }).format(date);     // "15 January 2024"
new Intl.RelativeTimeFormat("en", { numeric: "auto" }).format(-1, "day");        // "yesterday"
new Intl.ListFormat("en", { type: "conjunction" }).format(["a", "b", "c"]);      // "a, b, and c"
new Intl.PluralRules("en").select(1);                                             // "one" — pick the right message form
(1234.5).toLocaleString("en-US", { minimumFractionDigits: 2 });                   // shortcut to NumberFormat

Create a formatter once and reuse it — construction is expensive, format is cheap. Always pass a locale and, for dates, a timeZone; defaults come from the machine and differ between your laptop and the server. toFixed(2) is for machine-facing decimals (and rounds half-away-from-zero on the binary value, so (1.005).toFixed(2) is "1.00"); Intl is for humans.

Building text

Concatenation with +/+= and template literals is fine for ordinary sizes. For thousands of pieces, parts.push(…) then parts.join("\n") is clearer and allocation-friendly; for output measured in megabytes, write to a stream. Array.prototype.join on numbers calls String on each; [1, null, undefined].join("-") is "1--" — nullish elements become empty strings.

Escaping

Text inserted into another language needs that language's escaping: HTML (& < > " ' → entities), URLs (encodeURIComponent for components — never encodeURI for a value, never hand-rolled), shell (do not — use argument arrays), SQL (parameters, never strings), regex (escapeRegExp, module 10 lesson 4), JSON (JSON.stringify, which also gives you a correctly quoted string literal). A tagged template per target language is the tidy way to make escaping automatic.

Common mistakes

  • substring with negative arguments or reversed indices (silently "fixed") — use slice.
  • "".split(",") expected to be []; a trailing separator's empty element ignored.
  • replace when all occurrences were meant; $ sequences in a replacement built from user data.
  • Formatting numbers/dates by hand (toFixed, manual comma insertion) for a UI; creating an Intl formatter per call in a loop.
  • Interpolating objects/dates into templates and shipping [object Object] or a machine-locale date.
  • Escaping for the wrong target, or not at all, when building HTML/URLs.

Interview angle

  • "slice versus substring?" slice supports negatives and returns "" for reversed indices; substring clamps negatives to 0 and swaps reversed indices.
  • "What is a tagged template?" A function called with the literal's string parts and interpolated values, so it can escape, parameterise or transform them — html, sql, styled.
  • "How do you format 1234567.891 for German users?" new Intl.NumberFormat("de-DE").format(n) — never manual.
  • "replace versus replaceAll?" String pattern: first occurrence versus all; with a regex, replaceAll requires g.
  • "Why cache Intl formatters?" Construction is slow (locale data), format is fast.

Key takeaways

  • slice, includes/startsWith, split (know the empty-string cases), trim, padStart, replaceAll, at.
  • Template literals interpolate via String(); format explicitly for users.
  • Tagged templates receive (strings, ...values) — the mechanism behind html/sql/css tags and String.raw.
  • Intl.NumberFormat/DateTimeFormat/RelativeTimeFormat/ListFormat/PluralRules with an explicit locale (and timeZone), created once.
  • Escape for the target language — entities, encodeURIComponent, parameters, escapeRegExp — or use a tag that does it.

Common questions

What is the difference between slice and substring in JavaScript?

slice accepts negative indices, counting from the end, and returns an empty string when the start is after the end. substring treats negative arguments as 0 and silently swaps reversed arguments. Prefer slice: its behaviour is predictable, while substring's silent fixes hide bugs.

What is the difference between replace and replaceAll in JavaScript?

With a string pattern, replace changes only the first occurrence and replaceAll changes every one. With a regular expression, replace replaces all matches only if the regex has the g flag, and replaceAll requires that flag. Both interpret $ sequences in a string replacement, so pass a function when the replacement is user data.

How do you format a number with commas in JavaScript?

Use Intl.NumberFormat: new Intl.NumberFormat("en-US").format(1234567.891) returns "1,234,567.891", and the de-DE locale gives "1.234.567,891". The same API formats currencies, percentages, compact numbers and units. Create the formatter once and reuse it, because construction loads locale data while format is cheap.

What does "".split(",") return in JavaScript?

[""], an array holding one empty string, not an empty array. Likewise a trailing separator yields an empty last element: "a,b,".split(",") is ["a", "b", ""]. Check for empty input first, or filter out empty strings, when parsing a list.

Why does a template literal print [object Object]?

Interpolation converts each value with String(), and a plain object without its own toString becomes "[object Object]". Arrays join with commas, and null and undefined print as words. Format values explicitly, with JSON.stringify for debugging objects and Intl formatters for numbers and dates shown to users.

Mark as readSign in to keep your place.

JavaScript String Methods and Tagged Template Literals

JavaScript string methods precisely: slice vs substring, split edge cases, replace vs replaceAll, plus template literals, tagged templates and Intl formatting.

  • Course: JavaScript study plan
  • Module: Strings, Unicode, regular expressions and parsing
  • Kind: Lesson
  • Reading time: 12 min
  • Runtime: Node 16

What is a tagged template in JavaScript?

A tagged template is a template literal with a function name placed directly before it. JavaScript calls that function with the literal's string parts as an array and the interpolated values as separate arguments, before any concatenation, so the function decides what interpolation means: escaping HTML, turning values into SQL parameters, or building CSS. String.raw is a built-in tag that returns the raw text.

Lesson

Most string work is a dozen methods used well: find, slice, split, join, trim, pad, replace. The rest is formatting — turning numbers, dates and lists into text that is right for a reader — which is where hand-written code goes wrong in every locale but the author's, and where Intl gets it right for free. This lesson covers the methods with their exact semantics (slice versus substring, indexOf versus includes, split corner cases), template literals and the tagged-template mechanism that lets a function process a literal (the basis of html, sql and css tags), and the Intl formatters for numbers, currencies, dates and lists.

The methods, precisely

s.slice(2, 5); s.slice(-3);               // [start, end) — negatives count from the end; the one to use
s.substring(2, 5);                        // swaps arguments if start > end; treats negatives as 0 — legacy
s.indexOf("x"); s.lastIndexOf("x"); s.includes("x"); s.startsWith("x", from); s.endsWith("x", to);
s.at(-1);                                 // last code unit (Node 16.6+)
s.split(",");  s.split(",", 2);  s.split("");  s.split(/\s+/);
"a,b,".split(",");                        // ["a", "b", ""] — a trailing separator yields an empty last element
"".split(",");                            // [""] — not []
s.trim(); s.trimStart(); s.trimEnd();     // whitespace incl. Unicode spaces and line terminators
s.padStart(8, "0"); s.padEnd(20);         // to a target LENGTH (code units), truncating the pad if needed
s.repeat(3);                              // RangeError for negative or Infinity
s.replace("a", "b");                      // first occurrence only for a string pattern; $& $1 $<name> in the replacement
s.replaceAll("a", "b");                   // all (Node 15+); with a regex it must have the g flag
s.toUpperCase(); s.toLowerCase(); s.localeCompare(t); s.normalize();
s.concat(t) === s + t;  s.charAt(i) === s[i] (but "" instead of undefined out of range)

split with a regex containing a capture group includes the captures in the result — "a1b2c".split(/(\d)/) is ["a", "1", "b", "2", "c"], occasionally useful. replace with a string pattern and a string replacement interprets $ sequences ($$ for a literal dollar) — pass a function (() => text) when the replacement is user data.

Template literals

const line = `${user.name} has ${items.length} item${items.length === 1 ? "" : "s"}`;
const multi = `first line
second line`;                                            // real newlines preserved
`${1 + 1}`;  `${obj}`  /* calls toString */;  `${[1, 2]}` /* "1,2" */;  `${null}` /* "null" */

Interpolation calls String(value) (via the ToPrimitive rules of module 5): objects print [object Object] unless they define toString, arrays join with commas, undefined prints as undefined. For anything shown to users, format explicitly — an interpolated Date prints its long default form; an interpolated number prints without separators.

Tagged templates

A function placed before a template literal receives the literal parts and the values separately, before any concatenation:

function html(strings, ...values) {
  return strings.reduce((out, str, i) => out + str + (i < values.length ? escapeHtml(values[i]) : ""), "");
}
const safe = html`<p>${userInput}</p>`;      // strings = ["<p>", "</p>"], values = [userInput] — escaped automatically
String.raw`C:\new\table`;                    // "C:\\new\\table" — the raw text; strings.raw holds it in any tag

The tag decides what interpolation means: escaping (html), parameterising (sql\… WHERE id = ${id}\` → placeholders + parameters, no injection), styling (css, styled.div), localisation, GraphQL parsing. strings has a .raw property with the unescaped source text. Because the literal parts are known statically, a tag can cache parsing per call site — strings` is the same frozen array object on every evaluation of the same literal.

Intl: numbers, currency, dates, lists

new Intl.NumberFormat("en-US").format(1234567.891);                              // "1,234,567.891"
new Intl.NumberFormat("de-DE").format(1234567.891);                              // "1.234.567,891"
new Intl.NumberFormat("en-US", { style: "currency", currency: "EUR" }).format(9.5);   // "€9.50"
new Intl.NumberFormat("en", { notation: "compact" }).format(1_500_000);          // "1.5M"
new Intl.NumberFormat("en", { style: "percent", maximumFractionDigits: 1 }).format(0.1234);   // "12.3%"
new Intl.NumberFormat("en", { style: "unit", unit: "kilometer-per-hour" }).format(88);         // "88 km/h"
new Intl.DateTimeFormat("en-GB", { dateStyle: "long", timeZone: "UTC" }).format(date);     // "15 January 2024"
new Intl.RelativeTimeFormat("en", { numeric: "auto" }).format(-1, "day");        // "yesterday"
new Intl.ListFormat("en", { type: "conjunction" }).format(["a", "b", "c"]);      // "a, b, and c"
new Intl.PluralRules("en").select(1);                                             // "one" — pick the right message form
(1234.5).toLocaleString("en-US", { minimumFractionDigits: 2 });                   // shortcut to NumberFormat

Create a formatter once and reuse it — construction is expensive, format is cheap. Always pass a locale and, for dates, a timeZone; defaults come from the machine and differ between your laptop and the server. toFixed(2) is for machine-facing decimals (and rounds half-away-from-zero on the binary value, so (1.005).toFixed(2) is "1.00"); Intl is for humans.

Building text

Concatenation with +/+= and template literals is fine for ordinary sizes. For thousands of pieces, parts.push(…) then parts.join("\n") is clearer and allocation-friendly; for output measured in megabytes, write to a stream. Array.prototype.join on numbers calls String on each; [1, null, undefined].join("-") is "1--" — nullish elements become empty strings.

Escaping

Text inserted into another language needs that language's escaping: HTML (& < > " ' → entities), URLs (encodeURIComponent for components — never encodeURI for a value, never hand-rolled), shell (do not — use argument arrays), SQL (parameters, never strings), regex (escapeRegExp, module 10 lesson 4), JSON (JSON.stringify, which also gives you a correctly quoted string literal). A tagged template per target language is the tidy way to make escaping automatic.

Common mistakes

  • substring with negative arguments or reversed indices (silently "fixed") — use slice.
  • "".split(",") expected to be []; a trailing separator's empty element ignored.
  • replace when all occurrences were meant; $ sequences in a replacement built from user data.
  • Formatting numbers/dates by hand (toFixed, manual comma insertion) for a UI; creating an Intl formatter per call in a loop.
  • Interpolating objects/dates into templates and shipping [object Object] or a machine-locale date.
  • Escaping for the wrong target, or not at all, when building HTML/URLs.

Interview angle

  • "slice versus substring?" slice supports negatives and returns "" for reversed indices; substring clamps negatives to 0 and swaps reversed indices.
  • "What is a tagged template?" A function called with the literal's string parts and interpolated values, so it can escape, parameterise or transform them — html, sql, styled.
  • "How do you format 1234567.891 for German users?" new Intl.NumberFormat("de-DE").format(n) — never manual.
  • "replace versus replaceAll?" String pattern: first occurrence versus all; with a regex, replaceAll requires g.
  • "Why cache Intl formatters?" Construction is slow (locale data), format is fast.

Key takeaways

  • slice, includes/startsWith, split (know the empty-string cases), trim, padStart, replaceAll, at.
  • Template literals interpolate via String(); format explicitly for users.
  • Tagged templates receive (strings, ...values) — the mechanism behind html/sql/css tags and String.raw.
  • Intl.NumberFormat/DateTimeFormat/RelativeTimeFormat/ListFormat/PluralRules with an explicit locale (and timeZone), created once.
  • Escape for the target language — entities, encodeURIComponent, parameters, escapeRegExp — or use a tag that does it.

Common questions

What is the difference between slice and substring in JavaScript?

slice accepts negative indices, counting from the end, and returns an empty string when the start is after the end. substring treats negative arguments as 0 and silently swaps reversed arguments. Prefer slice: its behaviour is predictable, while substring's silent fixes hide bugs.

What is the difference between replace and replaceAll in JavaScript?

With a string pattern, replace changes only the first occurrence and replaceAll changes every one. With a regular expression, replace replaces all matches only if the regex has the g flag, and replaceAll requires that flag. Both interpret $ sequences in a string replacement, so pass a function when the replacement is user data.

How do you format a number with commas in JavaScript?

Use Intl.NumberFormat: new Intl.NumberFormat("en-US").format(1234567.891) returns "1,234,567.891", and the de-DE locale gives "1.234.567,891". The same API formats currencies, percentages, compact numbers and units. Create the formatter once and reuse it, because construction loads locale data while format is cheap.

What does "".split(",") return in JavaScript?

[""], an array holding one empty string, not an empty array. Likewise a trailing separator yields an empty last element: "a,b,".split(",") is ["a", "b", ""]. Check for empty input first, or filter out empty strings, when parsing a list.

Why does a template literal print [object Object]?

Interpolation converts each value with String(), and a plain object without its own toString becomes "[object Object]". Arrays join with commas, and null and undefined print as words. Format values explicitly, with JSON.stringify for debugging objects and Intl formatters for numbers and dates shown to users.

Exercises

An aligned report with Intl

Each line is name amount. Print an aligned table: a header Name | Amount | Share (name column padded to the longest name, at least 4; amount column right-aligned to the widest formatted amount, at least 6), a rule of dashes, one row per entry with the amount from Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }) and the share of the total as a percent with at most one decimal (padded to 5), a Total row, and finally compact=<total in compact notation> de=<total as EUR in de-DE> toFixed=<total.toFixed(2)>.

Example: rent 1200, coffee 47.5, cloud 1350000 →

Name   |        Amount | Share
-------+---------------+------
rent   |     $1,200.00 |  0.1%
coffee |        $47.50 |    0%
cloud  | $1,350,000.00 | 99.9%
Total  | $1,351,247.50 |  100%
compact=1.4M de=1.351.247,50 € toFixed=1351247.50

html and sql tags

Write two tag functions. html joins the literal parts and HTML-escapes every interpolated value (& < > " '). sql replaces every interpolation with $1, $2, … and returns { text, values }. Input: three lines — a name, a comment, an id. Print html\<p class="by">${name}</p><blockquote>${comment}</blockquote>\`, then the sql query SELECT * FROM comments WHERE author = ${name} AND id = ${Number(id)} as <text> -- <JSON values>, then a diagnostic tag applied to \a\\n${1}b${2}\ printing strings=<JSON> raw=<JSON of strings.raw> values=<JSON>, then raw=<String.raw\C:\\new\\table\> interpolated=<length of \C:\\new\>`.

Example: Ada <script>alert(1)</script> / I "love" it & more / 42 →

<p class="by">Ada &lt;script&gt;alert(1)&lt;/script&gt;</p><blockquote>I &quot;love&quot; it &amp; more</blockquote>
SELECT * FROM comments WHERE author = $1 AND id = $2 -- ["Ada <script>alert(1)</script>",42]
strings=["a\n","b",""] raw=["a\\n","b",""] values=[1,2]
raw=C:\new\table interpolated=5

In this module: Strings, Unicode, regular expressions and parsing

← Unicode and how JavaScript strings really work · Regular expressions — syntax, flags, groups and the five methods →