Implement isPalindrome in JS

Sunday 24/07/2022

·5 min read
Share:

A 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 true for 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 .split doesn'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.

Share:
VA

Vadim Alakhverdov

Software developer writing about JavaScript, web development, and developer tools.

Related Posts