Skip to main content

Command Palette

Search for a command to run...

String Methods

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

Published
6 min read
String Methods

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 /g

  • typeof "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 - 1

  • Chaining works: .trim().toLowerCase().split(",")

  • .split("") breaks a string into individual characters

  • Strings 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:

Simply JavaScript

Part 7 of 25

JavaScript is a quirky language. To master it, one should know to avoid its hidden traps along with its logic. This series showcase my journey through JS: the pain points, the breakthroughs, and the coding standards that I adopted from my mentors.

Up next

JavaScript Modules

Your app.js is getting out of hand