Skip to content

String and Number Methods

beginner18 min read

Strings Are Frozen in Time

Here's something that trips up almost every beginner: strings in JavaScript are immutable. You can't change a single character inside a string. Every method that looks like it's "modifying" a string is actually creating a brand new one and returning it.

let name = "alice";
name.toUpperCase();
console.log(name); // "alice" — unchanged!

name = name.toUpperCase();
console.log(name); // "ALICE" — reassigned to the new string

Why? Because strings are primitives, not objects. They live as fixed values in memory. When you call .toUpperCase(), the engine allocates a new string, copies the uppercased characters into it, and returns it. The original string is untouched.

Mental Model

Think of strings like printed receipts. You can't erase a letter on a receipt — you can only print a new receipt with the correction. Every string method hands you a fresh receipt and leaves the original in your pocket.

let greeting = "hello";
greeting[0] = "H";
console.log(greeting); // "hello" — silently ignored (strict mode throws)

This silent failure is one of the reasons you should always use "use strict" or TypeScript — strict mode would throw a TypeError here instead of silently doing nothing.

Quiz
What does this code log?

Essential String Methods

You don't need to memorize every string method — just the ones that show up in real code every day.

Finding Things

let sentence = "JavaScript is awesome and JavaScript is everywhere";

sentence.indexOf("JavaScript");     // 0 — first occurrence
sentence.indexOf("JavaScript", 1);  // 32 — search from index 1
sentence.indexOf("Python");         // -1 — not found

sentence.includes("awesome");       // true
sentence.startsWith("Java");        // true
sentence.endsWith("everywhere");    // true

indexOf returns the position (or -1 if not found). The includes, startsWith, and endsWith trio returns true/false — use these when you don't care about the position, just whether the match exists.

Extracting Pieces

let str = "Hello, World!";

str.slice(7, 12);      // "World" — start index to end index (exclusive)
str.slice(7);          // "World!" — from index 7 to the end
str.slice(-6);         // "orld!" — negative counts from the end

str.substring(7, 12);  // "World" — same as slice for positive indices
Common Trap

slice vs substring — just use slice. Always. It handles negative indices intuitively (counting from the end), while substring treats negatives as 0 and silently swaps arguments if start is greater than end. That "helpful" behavior creates bugs.

Trimming and Padding

let messy = "   hello   ";
messy.trim();       // "hello"
messy.trimStart();  // "hello   "
messy.trimEnd();    // "   hello"

"5".padStart(3, "0");   // "005" — pad to 3 chars with "0"
"hi".padEnd(10, ".");   // "hi........"

padStart is perfect for formatting numbers, timestamps, or IDs: String(hour).padStart(2, "0") turns 9 into "09".

Replacing and Repeating

let text = "foo bar foo baz foo";

text.replace("foo", "qux");     // "qux bar foo baz foo" — only first match
text.replaceAll("foo", "qux");  // "qux bar qux baz qux" — all matches

"ha".repeat(3);  // "hahaha"

Before replaceAll existed, people used regex with the global flag: text.replace(/foo/g, "qux"). Now replaceAll is cleaner for simple string replacements.

Splitting Strings into Arrays

"a,b,c".split(",");           // ["a", "b", "c"]
"hello".split("");            // ["h", "e", "l", "l", "o"]
"one two three".split(" ");   // ["one", "two", "three"]

split is the bridge between strings and arrays. Its counterpart is Array.prototype.join — they're inverses of each other.

Quiz
What does 'hello world'.slice(-5) return?

Template Literals

Template literals (the backtick strings) solve three problems that regular strings handle awkwardly.

Embedded Expressions

let name = "Alice";
let age = 30;

let bio = `${name} is ${age} years old and will be ${age + 1} next year.`;
// "Alice is 30 years old and will be 31 next year."

Any JavaScript expression works inside ${} — function calls, ternaries, math, even other template literals. But keep it simple. If your expression needs more than one line, extract it to a variable first.

Multiline Strings

let html = `
  <div>
    <h1>Title</h1>
    <p>Paragraph</p>
  </div>
`;

No more \n or string concatenation for multiline content. One thing to watch: the indentation inside the template literal becomes part of the string. If that matters (like in test assertions), use a library like dedent or trim the result.

Tagged Templates (Brief)

Tagged templates let you process template literal pieces with a function:

function highlight(strings, ...values) {
  return strings.reduce((result, str, i) => {
    return result + str + (values[i] !== undefined ? `**${values[i]}**` : "");
  }, "");
}

let name = "Alice";
highlight`Hello, ${name}!`;  // "Hello, **Alice**!"

You'll see tagged templates in libraries like styled-components, graphql-tag, and lit-html. You probably won't write your own often, but you should recognize the syntax when you see it.

Number Quirks You Must Know

JavaScript uses IEEE 754 double-precision floating-point for all numbers. This is the same format used by C's double, Java's double, and Python's float. It gives you about 15-17 significant decimal digits. Most of the time it works fine. But there are three quirks that bite everyone.

Quirk 1: Floating-Point Arithmetic

0.1 + 0.2;           // 0.30000000000000004
0.1 + 0.2 === 0.3;   // false

This isn't a JavaScript bug — it's a fundamental limitation of binary floating-point. The number 0.1 can't be represented exactly in binary, just like 1/3 can't be represented exactly in decimal (0.333...). The tiny rounding error accumulates.

Mental Model

Binary floating-point and decimal numbers are like metric and imperial measurements. Converting between them introduces tiny rounding errors. 1 inch is 2.54 cm exactly, but many conversions aren't so clean. Binary can represent 0.5 exactly (it's 0.1 in binary) but 0.1 becomes a repeating binary fraction — like how 1/3 is 0.333... in decimal.

The fix for money and precision-sensitive math:

// Work in cents (integers), not dollars (floats)
let priceInCents = 10 + 20;  // 30
let display = (priceInCents / 100).toFixed(2);  // "0.30"

// Or use Number.EPSILON for comparisons
Math.abs(0.1 + 0.2 - 0.3) < Number.EPSILON;  // true

Quirk 2: Safe Integer Limits

Number.MAX_SAFE_INTEGER;  // 9007199254740991 (2^53 - 1)

9007199254740991 + 1;  // 9007199254740992 — correct
9007199254740991 + 2;  // 9007199254740992 — WRONG! Same as +1

Number.isSafeInteger(9007199254740991);  // true
Number.isSafeInteger(9007199254740992);  // false

Beyond Number.MAX_SAFE_INTEGER, JavaScript can't distinguish between consecutive integers. This breaks database IDs, cryptocurrency values, and any large number work. Use BigInt for those cases: 9007199254740991n + 2n gives the correct result.

Quirk 3: NaN Is Not Equal to Itself

NaN === NaN;   // false
NaN == NaN;    // false
NaN !== NaN;   // true — the only value where this is true

This is mandated by the IEEE 754 spec, not a JavaScript quirk. The rationale: NaN represents "the result of a meaningless computation." Two different meaningless computations shouldn't be considered equal — 0/0 and Math.sqrt(-1) both produce NaN, but they're fundamentally different failures.

Quiz
What does 0.1 + 0.2 === 0.3 evaluate to?
Quiz
What does typeof NaN return?

Number Methods

Parsing Strings to Numbers

parseInt("42px");       // 42 — stops at first non-digit
parseInt("0xFF", 16);   // 255 — hex parsing
parseInt("111", 2);     // 7 — binary parsing
parseInt("hello");      // NaN

parseFloat("3.14rem");  // 3.14 — stops at first non-numeric char
parseFloat("1.2.3");    // 1.2 — stops at second dot
Common Trap

Always pass the radix (second argument) to parseInt. Without it, parseInt("08") used to parse as octal in older engines, returning 0. Modern engines default to base 10, but being explicit costs nothing and prevents surprises.

Formatting Numbers

let pi = 3.14159;

pi.toFixed(2);  // "3.14" — returns a STRING, not a number
pi.toFixed(0);  // "3"

// Common trap:
(1.005).toFixed(2);  // "1.00" — NOT "1.01"!
// Banker's rounding + floating-point imprecision

toFixed returns a string. If you need a number back, wrap it: Number(pi.toFixed(2)) or +pi.toFixed(2).

Checking Number Properties

// Number.isNaN vs global isNaN — know the difference
Number.isNaN(NaN);       // true
Number.isNaN("hello");   // false — no coercion
isNaN("hello");          // true — coerces "hello" to NaN first, then checks

// Number.isFinite — excludes Infinity and NaN
Number.isFinite(42);         // true
Number.isFinite(Infinity);   // false
Number.isFinite(NaN);        // false

// Number.isInteger
Number.isInteger(5);     // true
Number.isInteger(5.0);   // true — 5.0 is stored as 5
Number.isInteger(5.1);   // false
Number.isNaN vs isNaN — why two versions exist

The global isNaN() was the original, and it has a flaw: it coerces its argument to a number first. So isNaN("hello") coerces "hello" to NaN, then checks if it's NaN — returning true even though "hello" is a string, not NaN.

Number.isNaN() was added in ES2015 to fix this. It does zero coercion — it returns true only if the value is literally NaN. Always use Number.isNaN().

isNaN("hello");        // true — misleading!
Number.isNaN("hello"); // false — correct
Number.isNaN(NaN);     // true — the only true case
Quiz
What does Number.isNaN('hello') return?

The Math Object

Math is a namespace object (not a constructor — you can't do new Math()). It holds static methods for common mathematical operations.

Rounding

Math.floor(4.9);   // 4 — always rounds down
Math.ceil(4.1);    // 5 — always rounds up
Math.round(4.5);   // 5 — rounds to nearest, .5 rounds up
Math.trunc(4.9);   // 4 — chops off the decimal, no rounding

// Negative numbers — this is where they differ
Math.floor(-4.1);  // -5 — floor goes toward negative infinity
Math.ceil(-4.9);   // -4 — ceil goes toward positive infinity
Math.trunc(-4.9);  // -4 — trunc goes toward zero
Math.round(-4.5);  // -4 — rounds toward positive infinity at .5

The key distinction: Math.floor goes toward negative infinity, while Math.trunc goes toward zero. They're identical for positive numbers but diverge for negatives.

Min, Max, Abs

Math.max(1, 5, 3, 9, 2);  // 9
Math.min(1, 5, 3, 9, 2);  // 1
Math.abs(-42);             // 42

// With arrays — use spread
let scores = [85, 92, 78, 95, 88];
Math.max(...scores);  // 95

Random Numbers

Math.random();  // 0 to 0.999... (never reaches 1)

// Random integer between min (inclusive) and max (inclusive)
function randomInt(min, max) {
  return Math.floor(Math.random() * (max - min + 1)) + min;
}

randomInt(1, 6);  // simulates a dice roll
Math.random is not cryptographically secure

Never use Math.random() for security-sensitive purposes like generating tokens, passwords, or session IDs. Use crypto.getRandomValues() or crypto.randomUUID() instead.

Putting It All Together

Here's a real-world example that combines string and number methods — formatting a file size display:

function formatFileSize(bytes) {
  if (bytes === 0) return "0 B";

  let units = ["B", "KB", "MB", "GB", "TB"];
  let exponent = Math.min(
    Math.floor(Math.log(bytes) / Math.log(1024)),
    units.length - 1
  );
  let value = bytes / Math.pow(1024, exponent);

  return `${value.toFixed(1)} ${units[exponent]}`;
}

formatFileSize(0);            // "0 B"
formatFileSize(1024);         // "1.0 KB"
formatFileSize(1536000);      // "1.5 MB"
formatFileSize(5368709120);   // "5.0 GB"

This uses Math.floor, Math.min, Math.log, Math.pow, toFixed, and template literals — all in one practical function.

What developers doWhat they should do
Forgetting that string methods return new strings
Strings are immutable primitives — methods never modify the original
Always capture the return value: str = str.trim()
Comparing floats with === directly
IEEE 754 floating-point introduces tiny rounding errors that break strict equality
Use Math.abs(a - b) < Number.EPSILON or work with integers
Using isNaN() to check for NaN
Global isNaN coerces its argument first, so isNaN('hello') returns true even though 'hello' is a string
Use Number.isNaN() — no coercion
Assuming toFixed returns a number
toFixed always returns a string — (3.14).toFixed(1) gives '3.1' not 3.1
Wrap with Number() or unary + if you need a number
Using Math.round for negative numbers expecting symmetric behavior
Math.round(-0.5) returns 0 (toward positive infinity), which surprises people expecting -1
Use Math.trunc to always go toward zero
Key Rules
  1. 1Strings are immutable — every method returns a new string, never modifies the original
  2. 2Use slice over substring — it handles negative indices and has no surprising argument-swapping behavior
  3. 3Always pass a radix to parseInt: parseInt(str, 10) — being explicit prevents octal parsing surprises
  4. 4Never compare floats with === — use Math.abs(a - b) < Number.EPSILON or work in integer units like cents
  5. 5NaN is the only value not equal to itself — use Number.isNaN() (not the global isNaN) to detect it
  6. 6toFixed returns a string, not a number — wrap with Number() if you need numeric output
  7. 7Math.random is not cryptographically secure — use crypto.getRandomValues for security-sensitive randomness