String Polyfills and Common Interview Methods in JavaScript
"I thought I knew strings. Then an interview asked me to implement
.includes()from scratch โ and I froze."
๐ Why This Problem Matters
Here's the thing: JavaScript is everywhere. But JavaScript engines? Not all equal.
A feature like String.prototype.replaceAll wasn't added until ES2021. Before that, if you needed to replace all occurrences of a character in a string โ on an older browser, an old Node.js runtime, or a legacy environment โ you'd get:
TypeError: str.replaceAll is not a function
That's a real production bug. And it happens more than you think in banking apps, government portals, and IoT dashboards where the runtime is frozen years behind.
This is where polyfills come in. A polyfill is just a piece of code that says:
"Hey, if this method doesn't exist natively โ here, I'll build it for you."
At scale โ millions of users, hundreds of browser versions, dozens of environments โ polyfills aren't optional. They're survival tools.
๐ง Basic Concept โ What Even Is a String Method?
Before we implement anything, let's build intuition.
Think of a JavaScript string like a train. Each character is a passenger sitting in a numbered seat: seat 0, seat 1, seat 2... and so on till the end of the line.
A string method is like a train attendant who walks through and does something โ counts passengers, checks tickets, rearranges seating. When you call "hello".toUpperCase(), the attendant walks through and shouts every passenger's name louder.
Now: where do these methods live?
They live on String.prototype โ a shared blueprint that every string in JavaScript inherits from. It's like a rulebook that every train in the network follows.
console.log(typeof String.prototype.trim); // "function"
console.log(typeof String.prototype.includes); // "function"
This is important. Because when you write a polyfill, you're adding a new rule to that shared rulebook โ but only if the rule doesn't already exist.
โ ๏ธ Where Things Break โ The Polyfill Trap
Okay, here's where it gets interesting.
I assumed polyfills are just "backup code." Simple. Safe. Harmless.
I was wrong.
Here's a classic mistake beginners make. They write a polyfill like this:
String.prototype.includes = function(search) {
return this.indexOf(search) !== -1;
};
Do you see the problem?
They didn't check if includes already exists. So they're overwriting the native, optimized, spec-compliant version with a custom one. In some environments, the native version handles edge cases (like regex-like inputs, or Unicode normalization) that your version doesn't.
At scale โ imagine this running on a Node.js server handling 10,000 requests per second โ you've just silently degraded performance across your entire application. And the bug is nearly invisible.
The right pattern:
if (!String.prototype.includes) {
String.prototype.includes = function(search, start) {
if (typeof start !== 'number') start = 0;
return this.indexOf(search, start) !== -1;
};
}
The if (!String.prototype.includes) guard is the entire difference between safe and dangerous.
๐ฅ Aha Moment โ What Interviewers Are Actually Testing
This is where it clicked for me.
When an interviewer asks you to implement trim() or includes() from scratch, they don't care if you know the answer. They care about how you think.
They want to know:
Do you understand what the method is supposed to do? (spec knowledge)
Can you handle edge cases? (defensive thinking)
Do you reach for loops instinctively? (engineering intuition)
This is actually crazy ๐คฏ โ because the "simple" question "implement trim()" is hiding a systems design question: "Do you understand how primitive types and prototype chains work?"
Let me show you what I mean.
๐ ๏ธ Advanced Solutions โ Implementing the Core Methods
Let's go method by method. For each, I'll explain the intuition first, then the implementation.
1. String.prototype.trim()
Intuition: Imagine you have a sentence written on paper with a bunch of blank spaces on the left and right margins. Trim just removes those margins โ it doesn't touch anything in the middle.
What it does: Removes leading (left) and trailing (right) whitespace โ spaces, tabs (\t), newlines (\n).
if (!String.prototype.myTrim) {
String.prototype.myTrim = function() {
let start = 0;
let end = this.length - 1;
// Walk forward until we hit a non-whitespace character
while (start <= end && this[start] === ' ') start++;
// Walk backward until we hit a non-whitespace character
while (end >= start && this[end] === ' ') end--;
return this.slice(start, end + 1);
};
}
console.log(" hello world ".myTrim());
Edge case most people miss: What about tabs and newlines? The native trim() handles all whitespace (\t, \n, \r). A robust polyfill uses a regex test:
String.prototype.myTrimFull = function() {
return this.replace(/^\s+|\s+$/g, '');
};
This is what trim() does under the hood โ it's essentially a regex operation on both ends.
2. String.prototype.includes()
Intuition: Think of includes() like using Ctrl+F in a browser. You type a word, and the browser scans the entire page to see if it finds that word anywhere.
What it does: Returns true if the search string exists anywhere inside the main string, false otherwise.
if (!String.prototype.myIncludes) {
String.prototype.myIncludes = function(search, startIndex) {
if (search === undefined) return false;
if (typeof startIndex !== 'number') startIndex = 0;
const str = String(this);
const searchStr = String(search);
// Can't find something longer than what we're searching in
if (searchStr.length > str.length) return false;
for (let i = startIndex; i <= str.length - searchStr.length; i++) {
if (str.slice(i, i + searchStr.length) === searchStr) {
return true;
}
}
return false;
};
}
console.log("hello world".myIncludes("world")); // true
console.log("hello world".myIncludes("xyz")); // false
console.log("hello world".myIncludes("world", 7)); // false (starts searching from index 7)
The startIndex parameter is what most candidates forget. The native includes() accepts a second argument โ where to begin the search. Miss that in an interview, and you've missed a spec detail.
3. String.prototype.startsWith()
Intuition: Like checking if someone's name on an attendance list starts with "Saurabh." You don't read the whole entry โ just the first few characters.
if (!String.prototype.myStartsWith) {
String.prototype.myStartsWith = function(search, position) {
const str = String(this);
if (typeof position !== 'number') position = 0;
return str.slice(position, position + search.length) === search;
};
}
console.log("javascript".myStartsWith("java")); // true
console.log("javascript".myStartsWith("script", 4)); // true
Notice the clean logic: slice exactly search.length characters from the position, then compare. One slice, one comparison. That's it.
4. String.prototype.repeat()
Intuition: Like a copy machine. You feed in one page and tell it: "give me 5 copies."
if (!String.prototype.myRepeat) {
String.prototype.myRepeat = function(count) {
count = Math.floor(count);
if (count < 0) throw new RangeError('Invalid count value');
if (count === 0) return '';
let result = '';
for (let i = 0; i < count; i++) {
result += this;
}
return result;
};
}
console.log("ha".myRepeat(3)); // "hahaha"
console.log("*".myRepeat(5)); // "*****"
Interview insight: This is O(n ร k) โ linear in both the string length and the count. At scale, repeating large strings thousands of times can be a memory problem. A smarter approach doubles the string repeatedly (like exponentiation by squaring), but interviewers rarely expect that unless they hint toward optimization.
5. String.prototype.replaceAll() โ The Tricky One
Intuition: The classic replace() is like a search-and-replace that stops after the first match. replaceAll() keeps going until there's nothing left to replace โ like a global search-replace in a code editor.
if (!String.prototype.myReplaceAll) {
String.prototype.myReplaceAll = function(search, replacement) {
// Escape special regex characters in the search string
const escaped = String(search).replace(/[.*+?^\({}()|[\]\\]/g, '\\\)&');
return this.replace(new RegExp(escaped, 'g'), replacement);
};
}
console.log("cat and cat and cat".myReplaceAll("cat", "dog"));
// "dog and dog and dog"
Where I went wrong initially: I tried to do this with a while loop and indexOf. It works but it's error-prone โ if your replacement string contains the search string, you get an infinite loop. The regex approach with 'g' flag is cleaner and is actually how browsers implement it internally.
6. Common Interview Problem โ Reverse a String
Not a built-in method, but this comes up constantly.
function reverseString(str) {
// Naive: split โ reverse โ join
return str.split('').reverse().join('');
}
// Without built-ins:
function reverseStringManual(str) {
let result = '';
for (let i = str.length - 1; i >= 0; i--) {
result += str[i];
}
return result;
}
console.log(reverseString("javascript")); // "tpircsavaj"
Depth question: What about Unicode? Emoji and multi-byte characters can break naive reversal because they span multiple character codes.
"๐abc".split('').reverse().join(''); // "cba๐" โ โ
works here, but...
"๐จโ๐ฉโ๐ง".split('').reverse().join(''); // โ garbled family emoji
This is a ๐คฏ moment โ the naive solution fails on emoji families because they use multiple Unicode code points joined with invisible "zero-width joiner" characters. The spec-correct solution uses the spread operator with Unicode-aware iteration:
[..."๐จโ๐ฉโ๐ง".split(/(?:)/u)].reverse().join('');
A senior interviewer might push you here. I wish I knew this earlier.
7. Count Occurrences of a Character
Another classic interview question that tests your understanding of iteration and string indexing.
function countOccurrences(str, char) {
let count = 0;
for (let i = 0; i < str.length; i++) {
if (str[i] === char) count++;
}
return count;
}
// Or one-liner using split:
function countOccurrences2(str, char) {
return str.split(char).length - 1;
}
console.log(countOccurrences("banana", "a")); // 3
Insight: The split trick is clever but has a conceptual overhead โ you're creating an entire array just to count. For very long strings, the loop version is more memory-efficient.
โ๏ธ Tradeoffs โ When to Write Polyfills, When to Skip
Let me be honest: polyfills aren't always the right call.
| Situation | Use Polyfill? |
|---|---|
| Supporting browsers older than 5+ years | Yes โ absolutely |
| Modern apps with Babel/core-js in the build pipeline | No โ Babel handles this |
| A quick internal tool | No โ adds maintenance burden |
| A public-facing library used by others | Yes โ you can't control their environments |
| Performance-critical hot paths | Be cautious โ test against native performance |
Performance vs consistency: A handwritten polyfill is almost never as fast as the native implementation, which is usually written in C++ inside the V8 engine. So you're trading speed for compatibility.
Simplicity vs correctness: The simple version of trim() (just removing spaces) might miss edge cases like non-breaking spaces (\u00A0). The correct version uses a regex โ which is less readable but more compliant with the spec.
๐ Real-World Usage
Babel + core-js is how modern teams handle this at scale. When you write "hello".includes("ell") and Babel transpiles it, core-js automatically injects the polyfill for environments that need it โ and skips it where it's not needed.
At IBM, we work on Maximo โ an enterprise asset management platform that runs on some environments where you can't always guarantee the latest JS runtime. Understanding what polyfills do (even if Babel handles them for us) helps us debug weird runtime issues and write code that doesn't surprise us in production.
Browser support tools like Browserslist let you define which browsers your app supports. Combined with a bundler, polyfills are injected selectively โ so modern Chrome users don't download KB of unnecessary compatibility code, while users on older browsers get exactly what they need.
๐งพ Final Summary
String methods live on
String.prototypeโ a shared blueprint for all strings.Polyfills inject custom implementations only when native ones don't exist.
Always guard with
if (!String.prototype.method)before injecting.Common interview methods:
trim,includes,startsWith,repeat,replaceAll.Edge cases are the real test: Unicode, empty strings, negative indices, non-string inputs.
At scale: use Babel + core-js โ don't hand-roll polyfills in production apps unless you have very specific needs.
๐ Personal Reflection
What surprised me most isn't the code โ it's what these questions are really asking.
When an interviewer says "implement includes()", they're not running a trivia quiz. They're checking: do you think about the thing before you build it? Do you consider edge cases? Do you understand the contract your code makes?
I used to use string methods like appliances โ plug in, press button, get result. Now I think about them differently. They're tiny algorithms. Each one is a small engineering decision made by someone who thought hard about performance, correctness, and API design.
That's what I wish I knew before that interview. And honestly? Getting stumped was the best thing that happened to me โ because it sent me down this rabbit hole.
Understanding the internals makes you better at using the externals. Every time.
Written by Saurabh Prajapati โ Software Engineer @ IBM India Software Lab - WebDev Cohort 2026



