String Polyfills and Common Interview Methods in JavaScript
Most developers use string methods without thinking twice. Here's what's actually happening under the hood — and how to rebuild it yourself.

Most developers treat string methods like magic. This guide peels back the curtain — you'll implement them from scratch, understand why polyfills exist, and actually be ready when an interviewer says "no built-ins allowed."
What is a String Method, Really?
When you call "hello".toUpperCase(), JavaScript isn't doing anything mystical. It's just looking up a function on String.prototype. Every string in JS shares that same prototype, which is why all of them get access to trim, includes, split, and the rest.
One thing to burn into your brain early: strings are immutable. Every method you call returns a brand-new string — it never touches the original. That's not a quirk, it's the design.
Why Write a Polyfill?
A polyfill is a fallback. When a newer method doesn't exist in an older browser or runtime, a polyfill adds it manually. You've probably used them without knowing — Babel and bundlers inject them all the time.
But the real reason to write them yourself is simpler: it forces you to understand what the method actually does. You can't fake your way through implementing trim() — You have to think about what "whitespace" even means, where to start, and where to stop.
Interview pattern:
Polyfills are a favourite in JS interviews because they expose whether you understand the language or just its surface API. "Implement
split()without usingsplit()" tells an interviewer a lot more than "usesplit()on this array."
The standard shape of any polyfill looks like this — always check first so you don't overwrite a faster native version:
if (!String.prototype.methodName) {
String.prototype.methodName = function() {
// your implementation here
};
}
Core Polyfills, Built from Scratch
1. trim()
Two pointers — one from the left, one from the right. Walk them inward until you hit a non-whitespace character, then slice between them. Clean, O(n), and easy to explain.
if (!String.prototype.trim) {
String.prototype.trim = function() {
let start = 0;
let end = this.length - 1;
while ([' ', '\n', '\t'].includes(this[start])) start++;
while ([' ', '\n', '\t'].includes(this[end])) end--;
return this.slice(start, end + 1);
};
}
2. includes()
Slide a window of the same length as your search string across the main string. At each position, check if the slice matches. Return true the moment it does, false if you reach the end.
if (!String.prototype.includes) {
String.prototype.includes = function(search, start = 0) {
if (search instanceof RegExp)
throw new TypeError('First argument must not be a RegExp');
for (let i = start; i <= this.length - search.length; i++) {
if (this.slice(i, i + search.length) === search) return true;
}
return false;
};
}
3. repeat()
Straightforward loop — concatenate the string onto itself count times. Just validate the edge cases first.
if (!String.prototype.repeat) {
String.prototype.repeat = function(count) {
if (count < 0 || count === Infinity)
throw new RangeError('Invalid count value');
let result = '';
count = Math.floor(count);
for (let i = 0; i < count; i++) result += this;
return result;
};
}
4. split() — the interview classic
This one trips people up because of the edge cases: empty separator, undefined separator, and the limit parameter. Work through each case explicitly.
if (!String.prototype.split) {
String.prototype.split = function(separator, limit) {
const result = [];
if (separator === '') {
for (let i = 0; i < this.length; i++) {
if (limit !== undefined && result.length >= limit) break;
result.push(this[i]);
}
return result;
}
if (separator === undefined) return [this.toString()];
let curr = '';
const sepLen = separator.length;
for (let i = 0; i < this.length; i++) {
if (limit !== undefined && result.length >= limit) return result;
if (this.slice(i, i + sepLen) === separator) {
result.push(curr);
curr = '';
i += sepLen - 1;
} else { curr += this[i]; }
}
if (limit === undefined || result.length < limit) result.push(curr);
return result;
};
}
How split() Actually Works
Here's what's happening character by character when you call "a,b,c".split(","):
Classic Interview Problems
These come up constantly. The polyfill thinking — iterating character by character, tracking state manually — applies directly to all of them.
| Problem | Core idea |
|---|---|
| reverse() | Loop from length-1 down to 0, build a new string |
| isPalindrome() | Two pointers — compare str[i] vs str[n-1-i] until midpoint |
| capitalize() | Check ASCII range 97–122 (a–z), subtract 32 for uppercase |
| countVowels() | Loop each char, check if 'aeiou' contains it |
| removeDupes() | Track seen characters in an object/Set, only add unseen ones |
| truncate() | slice(0, max) + append '...' if needed |
Always state these three things for every interview solution: time complexity (usually O(n) for a single pass), space complexity (O(n) for the new string you're building), and the edge cases you've handled — empty string, single character, all whitespace.
A Deeper Look: Capitalize Without Built-ins
This comparison shows exactly what polyfill thinking buys you. Compare the two approaches to capitalizing a word:
// Surface-level: works, but you don't know why
const cap1 = word.charAt(0).toUpperCase() + word.slice(1);
// Deeper: you know what toUpperCase() is actually doing
const code = word.charCodeAt(0);
const cap2 = (code >= 97 && code <= 122)
? String.fromCharCode(code - 32) + word.slice(1)
: word;
The second version isn't better for production code — the first is perfectly fine. But understanding why subtracting 32 works (because uppercase letters occupy ASCII positions 65–90, exactly 32 below their lowercase equivalents at 97–122) is the difference between using the language and knowing it.


