String and Number Methods
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.
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.
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
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.
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.
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.
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
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 caseThe 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
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 do | What 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 |
- 1Strings are immutable — every method returns a new string, never modifies the original
- 2Use slice over substring — it handles negative indices and has no surprising argument-swapping behavior
- 3Always pass a radix to parseInt: parseInt(str, 10) — being explicit prevents octal parsing surprises
- 4Never compare floats with === — use Math.abs(a - b) < Number.EPSILON or work in integer units like cents
- 5NaN is the only value not equal to itself — use Number.isNaN() (not the global isNaN) to detect it
- 6toFixed returns a string, not a number — wrap with Number() if you need numeric output
- 7Math.random is not cryptographically secure — use crypto.getRandomValues for security-sensitive randomness