Implement isPalindrome in JS
Sunday 24/07/2022
·5 min readA palindrome is a string that reads the same forward and backward — "racecar", "level", "madam". Writing isPalindrome is a five-line problem if you allow ES6 string methods, a twenty-line problem if you optimize for performance, and a one-line problem if you don't care about anything except correctness. Below: three implementations, when to use each, and the edge cases (whitespace, punctuation, case) that always trip up the naive version.
1. Reverse and compare — the one-liner
The simplest approach: reverse the string and check equality.
function isPalindrome(str) {
return str === str.split('').reverse().join('')
}
isPalindrome('racecar') // true
isPalindrome('hello') // false
isPalindrome('a') // true
isPalindrome('') // true
What's happening: split('') turns the string into a char array, reverse() flips it in place, join('') reassembles it as a string. Strict-equality compares character by character.
This works for ASCII but breaks on multibyte characters (emoji, certain CJK characters). 'a🦄a'.split('') produces a four-element array because emoji are two UTF-16 code units. Use [...str] for a code-point-aware split:
function isPalindrome(str) {
return str === [...str].reverse().join('')
}
The spread operator iterates by Unicode code point, not UTF-16 unit. For emoji-safe palindrome checks this is the version you want.
2. Two-pointer scan — the efficient one
The one-liner allocates a new array and a new string. For long inputs in a hot path, walk inward from both ends and bail on the first mismatch:
function isPalindrome(str) {
let left = 0
let right = str.length - 1
while (left < right) {
if (str[left] !== str[right]) return false
left++
right--
}
return true
}
O(n/2) comparisons and zero extra allocation. For a 1 MB string, this is ~10× faster than the reverse-and-compare version, and the speedup matters when you're calling it in a tight loop.
For Unicode safety, convert to a code-point array once:
function isPalindrome(str) {
const chars = [...str]
let left = 0
let right = chars.length - 1
while (left < right) {
if (chars[left] !== chars[right]) return false
left++
right--
}
return true
}
3. Recursive — for the interview
If the interviewer wants recursion, give them this:
function isPalindrome(str) {
if (str.length <= 1) return true
if (str[0] !== str[str.length - 1]) return false
return isPalindrome(str.slice(1, -1))
}
Each call strips the first and last characters and recurses. Pretty, but allocates a new substring on every level — O(n²) total work and a stack frame per character. Don't use this in production.
Handling real-world input
Most palindrome checks need to ignore case, spaces, and punctuation. A standard pre-processing step:
function isPalindrome(str) {
const cleaned = str
.toLowerCase()
.replace(/[^a-z0-9]/g, '')
let left = 0
let right = cleaned.length - 1
while (left < right) {
if (cleaned[left] !== cleaned[right]) return false
left++
right--
}
return true
}
isPalindrome("A man, a plan, a canal: Panama") // true
isPalindrome("Was it a car or a cat I saw?") // true
The regex [^a-z0-9] strips everything that isn't ASCII alphanumeric. For Unicode-aware cleaning (covering accented characters and CJK), use \p{L}\p{N} with the u flag:
const cleaned = str.toLowerCase().replace(/[^\p{L}\p{N}]/gu, '')
Edge cases worth knowing
- Empty string: every implementation returns
true. That's the conventional answer — an empty string trivially equals its reverse. - Single character: also
truefor the same reason. - Case sensitivity:
"Racecar"is not a palindrome by strict equality. Lowercase first if that's not what you want. - Whitespace:
" racecar "(with leading/trailing space) is also not a palindrome strictly. Trim and clean if you're checking phrases. - Multi-byte characters: emoji break naive
.split('')because UTF-16 surrogate pairs get split. Use[...str]for code-point iteration. - Numbers and booleans:
isPalindrome(121)will fail because.splitdoesn't exist on numbers. Coerce to string first:isPalindrome(String(input)).
Picking the right implementation
| You need | Use |
|---|---|
| Quick and clear | Reverse and compare with [...str] |
| Performance on long strings | Two-pointer scan |
| Phrase / sentence checking | Pre-clean then two-pointer |
| Interview answer | Two-pointer (recursion if asked) |
FAQ
What's the time complexity of the reverse-and-compare approach?
O(n) for the split, O(n) for the reverse, O(n) for the join, O(n) for the equality check — O(n) overall, but with three allocations and roughly 3× the constant factor of a two-pointer scan.
Why doesn't str.split('') work for emoji?
JavaScript strings are UTF-16 code units, not Unicode code points. Emoji like 🦄 are represented as two UTF-16 surrogate halves. .split('') splits between the halves, breaking the character. Use [...str] or Array.from(str) to iterate by code point.
How do I check if a number is a palindrome?
Convert to a string first: isPalindrome(String(121)). Or, mathematically, reverse the digits with % 10 and / 10 arithmetic — useful in environments without string methods.
Should I lowercase the string before checking?
Depends on intent. For phrase palindromes ("A man, a plan, a canal: Panama"), yes — lowercase and strip non-alphanumeric. For strict palindrome checks of arbitrary strings, no — "Aa" is genuinely not a palindrome if you care about case.