String Methods
Decoding built-in methods, polyfills, and character-by-character logic

When you write "hello" in JavaScript, you're not just storing letters. You get an object with a bunch of methods sitting on it. Things like .toUpperCase(), .includes(), .slice(). You can call them immediately, without any setup.
Most beginners use these without thinking about what's happening underneath. That works fine until you're in an interview and someone asks you to implement .includes() yourself. Or until you hit a weird edge case and don't understand why the built-in method behaved the way it did.
Strings in JavaScript are immutable. No string method changes the original. Every method returns a new value. If you don't capture that return value, it's gone.
How Built-In Methods Actually Work
JavaScript loops through characters, checks conditions, and builds a result. .includes() is a loop that returns true the moment it finds a match. .toUpperCase() walks every character and shifts it to uppercase using Unicode. .trim() scans from both ends and stops when it hits a non-space character.
When you understand that, weird behavior starts making sense. Why does .indexOf() return -1 when it finds nothing? Because it's returning the position of the match, and there's no position that means "not found" other than an impossible one. Why does .slice(-3) count from the end? Because the method is designed to accept negative indices as an offset from the string's length.
This matters because it changes how you read code. When you see .slice(6) you know it starts a loop at index 6. When you see .replace(/\s+/g, " ") you know it runs through the string once replacing every cluster of whitespace. They're loops with return values.
The Methods You'll Actually Use
There are 30-something string methods in JavaScript. Most of them you'll call once in your life. A handful you'll use every day. Here are the ones that matter.
Slicing and Searching
const s = "hello world";
// slice(start, end): end is exclusive. negative counts from the back.
s.slice(0, 5) // "hello"
s.slice(6) // "world"
s.slice(-5) // "world" — 5 from the end
// indexOf: returns position or -1. case sensitive.
s.indexOf("world") // 6
s.indexOf("xyz") // -1
// includes: just want true/false? use this instead of indexOf.
s.includes("world") // true
// startsWith / endsWith
s.startsWith("hello") // true
s.endsWith("world") // true
Transforming
const s = " Hello World ";
// Case
s.toUpperCase() // " HELLO WORLD "
s.toLowerCase() // " hello world "
// Trim whitespace: trim() does both ends, trimStart/trimEnd do one side
s.trim() // "Hello World"
s.trimStart() // "Hello World "
// Replace: replace() only hits the first match. replaceAll() gets every one.
"aabbcc".replace("b", "x") // "axbcc" — only first b
"aabbcc".replaceAll("b", "x") // "aaxxcc" — both
// Split and join: two sides of the same coin
"a,b,c".split(",") // ["a","b","c"]
["a","b","c"].join("-") // "a-b-c"
// Padding: useful for formatting numbers and IDs
"7".padStart(3, "0") // "007"
"7".padEnd(3, ".") // "7.."
// Repeat
"na".repeat(4) // "nananana"
One thing that trips beginners: .replace() only replaces the first match. If you want all of them, use .replaceAll() or pass a regex with the g flag: .replace(/b/g, "x").
Polyfills: When The String Methods are Missing
A polyfill is just a manual implementation of a built-in method. You write it so an environment that doesn't have it can still run code that uses it. Older browsers are the typical reason, but polyfills show up in interviews for a different reason: if you can write one, you actually understand what the method does.
Let's see the example for trim polyfill:
function myTrim(str) {
let start = 0;
let end = str.length - 1;
// walk inward from both sides until we hit a non-space
while (start <= end && str[start] === " ") start++;
while (end >= start && str[end] === " ") end--;
return str.slice(start, end + 1);
}
myTrim(" hello ") // "hello"
myTrim(" ") // ""
Here is the implementation of includes through polyfill:
// .includes(searchStr, fromIndex)
function myIncludes(str, search, from = 0) {
// normalize negative fromIndex
const start = from < 0
? Math.max(0, str.length + from)
: from;
for (let i = start; i <= str.length - search.length; i++) {
if (str.slice(i, i + search.length) === search) {
return true;
}
}
return false;
}
myIncludes("hello world", "world") // true
myIncludes("hello world", "xyz") // false
The Hiccup in Interview
Interviews like string problems because they test whether you can think character-by-character. You can't memorize your way through these. You need to actually understand how strings are structured.
Here are four problems that come up repeatedly. Each one builds on a concept from the methods above
Reverse a String
// split into chars, reverse the array, join back.
function reverseString(str) {
return str.split("").reverse().join("");
}
// without array methods
function reverseManual(str) {
let result = "";
for (let i = str.length - 1; i >= 0; i--) {
result += str[i];
}
return result;
}
reverseString("hello") // "olleh"
String is a Palindrome
// A string that reads the same forward and backward.
function isPalindrome(str) {
const cleaned = str.toLowerCase().replace(/[^a-z0-9]/g, "");
return cleaned === cleaned.split("").reverse().join("");
}
isPalindrome("racecar") // true
isPalindrome("A man a plan a canal Panama") // true (after cleanup)
isPalindrome("hello") // false
Count Occurrences of a Character
function countChar(str, char) {
let count = 0;
for (const c of str) {
if (c === char) count++;
}
return count;
}
// One-liner using split
const countChar2 = (str, char) => str.split(char).length - 1;
countChar("hello", "l") // 2
Capitalize First Letter of Each Word
function titleCase(str) {
return str
.split(" ")
.map(word => word[0].toUpperCase() + word.slice(1).toLowerCase())
.join(" ");
}
titleCase("the quick brown fox") // "The Quick Brown Fox"
Why Built-In Behavior Matters
JavaScript's string methods are not always consistent in how they handle edge cases. Knowing what to expect saves you a debugging session at the wrong moment.
The Gotchas
"".split("x")returns[""], not[].indexOf()is case-sensitive."Hello".indexOf("h")is-1.slice(2, 0)returns"". End before start is empty.replace()only replaces the first match without/gtypeof "hello"is"string", not"object"
The Knowledge for the Long Run
Template literals avoid a lot of
.replace()calls.at(-1)gets the last character without.length - 1Chaining works:
.trim().toLowerCase().split(",").split("")breaks a string into individual charactersStrings are iterable:
for (const c of str)works
// split on empty string : array of individual chars
"abc".split("") // ["a","b","c"]
"".split("") // [] | empty string, empty array
"".split("x") // [""] | one empty string element. catches people out.
// indexOf vs includes
"hello".indexOf("H") // -1 | case sensitive
"hello".includes("H") // false | also case sensitive
// slice with inverted indices
"hello".slice(3, 1) // "" | start after end. empty
"hello".slice(-2) // "lo" | last 2 chars
What You Actually Get From Understanding This
REFERENCES:




