JavaScript 字符串常见面试题:反转、回文与最多删除一个字符
字符串题是前端面试里很常见的一类题。它们通常不会考特别复杂的语法,而是考你对基础 API、指针思想、边界条件和时间复杂度的理解。
本文结合几个常见例子,梳理 3 类高频字符串问题:
- 如何反转字符串
- 如何判断字符串是否为回文字符串
- 如何判断一个字符串最多删除一个字符后能否成为回文字符串
同时也会顺带讲清楚 JavaScript 字符串、包装类型、call 和 Object.prototype.toString.call 这些容易被问到的基础点。
一、字符串反转
JavaScript 的字符串本身没有 reverse() 方法。
也就是说,下面这样写是错误的:
const str = "abc";
str.reverse(); // TypeError: str.reverse is not a function
因为 reverse() 是数组的方法,不是字符串的方法。
常见写法是:
const str = "abc";
const result = str.split("").reverse().join("");
console.log(result); // "cba"
这行代码分成三步:
str.split("") // ["a", "b", "c"]
.reverse() // ["c", "b", "a"]
.join(""); // "cba"
完整函数可以写成:
function reverseString(str) {
return str.split("").reverse().join("");
}
console.log(reverseString("hello")); // "olleh"
这个方法简单直接,适合大多数普通字符串面试题。
需要注意:split("") 是按 UTF-16 码元拆分的,如果字符串里包含某些 emoji,可能会把一个字符拆坏。普通面试题一般不考这个细节,如果要比 split("") 更稳妥,可以使用 Array.from(str),它会按 Unicode 码点遍历:
function reverseString(str) {
return Array.from(str).reverse().join("");
}
二、为什么字符串可以访问 length 和方法?
字符串是基本数据类型:
const str = "abc";
严格来说,str 不是对象。但我们却可以这样访问:
console.log(str.length); // 3
console.log(str.toUpperCase()); // "ABC"
原因是 JavaScript 在访问基本类型的属性或方法时,会临时把它包装成对应的包装对象。
可以理解为底层临时做了类似这样的事:
const temp = new String("abc");
temp.length;
temp.toUpperCase();
用完之后,这个临时包装对象就会被释放,原来的 str 仍然是基本类型字符串。
所以更准确的说法是:
string是基本类型String是包装类型- 基本类型在访问属性或方法时,会发生临时装箱
示例:
const str1 = "abc";
const str2 = new String("abc");
console.log(typeof str1); // "string"
console.log(typeof str2); // "object"
实际开发中,通常不要主动写 new String("abc"),直接使用字符串字面量即可。
三、toString、JSON.stringify 和 Object.prototype.toString.call 的区别
这几个 API 容易混在一起,但它们的用途不一样。
1. x.toString()
toString() 通常用于把一个值转换成字符串。
不同类型可以有不同的实现:
console.log([1, 2, 3].toString()); // "1,2,3"
console.log((123).toString()); // "123"
普通对象默认的 toString() 结果不太适合展示对象内容:
const obj = { a: 1, b: 2 };
console.log(obj.toString()); // "[object Object]"
2. JSON.stringify()
如果想把对象或数组的内容序列化成 JSON 字符串,应该使用 JSON.stringify():
const obj = { a: 1, b: 2 };
console.log(JSON.stringify(obj)); // '{"a":1,"b":2}'
3. Object.prototype.toString.call()
Object.prototype.toString.call(value) 常用于判断更精确的类型标签:
console.log(Object.prototype.toString.call("abc")); // "[object String]"
console.log(Object.prototype.toString.call([])); // "[object Array]"
console.log(Object.prototype.toString.call({})); // "[object Object]"
console.log(Object.prototype.toString.call(null)); // "[object Null]"
console.log(Object.prototype.toString.call(undefined)); // "[object Undefined]"
所以它更多是类型判断工具,而不是用来序列化对象内容。
四、call:改变函数执行时的 this
call 的作用是:调用函数,并指定函数运行时的 this 指向。
在 JavaScript 中,函数是一等公民,也可以像对象一样拥有属性和方法。call 就是函数可以访问到的方法之一,它来自 Function.prototype。
看一个例子:
const user1 = {
name: "落月",
say() {
console.log(this.name);
}
};
const user2 = {
name: "落叶"
};
user1.say(); // "落月"
user1.say.call(user2); // "落叶"
user1.say.call(user2) 的意思是:
- 执行
user1.say这个函数 - 函数内部的
this指向user2
所以最终打印的是 user2.name。
注意,call 第一个参数是 this 指向,后面的参数才是传给函数本身的参数。
function introduce(age, city) {
console.log(`${this.name}, ${age}, ${city}`);
}
const user = {
name: "落月"
};
introduce.call(user, 21, "杭州"); // "落月, 21, 杭州"
五、判断一个字符串是否为回文字符串
回文字符串指的是:正着读和反着读都一样的字符串。
例如:
yessey
level
abba
方法一:反转后比较
最直观的写法是先反转,再和原字符串比较:
function isPalindrome(str) {
return str === str.split("").reverse().join("");
}
console.log(isPalindrome("level")); // true
console.log(isPalindrome("hello")); // false
这种写法很好理解,但它需要额外创建数组和反转后的字符串。
时间复杂度:O(n)
空间复杂度:O(n)
方法二:双指针
回文字符串的核心特征是左右对称。
所以可以用两个指针:
left从左往右走right从右往左走- 每次比较
str[left]和str[right] - 只要有一组不相等,就不是回文
代码如下:
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;
}
console.log(isPalindrome("level")); // true
console.log(isPalindrome("reser")); // true
console.log(isPalindrome("hello")); // false
也可以使用 for 循环:
function isPalindrome(str) {
const len = str.length;
for (let i = 0; i < len / 2; i++) {
if (str[i] !== str[len - i - 1]) {
return false;
}
}
return true;
}
这里提前保存 len 是一个好习惯,代码更清晰,也避免在循环中反复写 str.length。不过现代 JavaScript 引擎对这类访问通常已经有优化,不必把它理解成很大的性能差异。
双指针版本的复杂度:
- 时间复杂度:
O(n) - 空间复杂度:
O(1)
面试时更推荐说双指针方案,因为它不需要额外反转字符串。
六、最多删除一个字符后是否能成为回文字符串
这是回文判断的经典变形题。
题目描述:
给定一个非空字符串 s,最多删除一个字符,判断它是否能成为回文字符串。
例如:
"aba" -> true,不需要删除
"abca" -> true,删除 b 或 c 后变成 "aca" 或 "aba"
"abc" -> false,删除任意一个字符都不能变成回文
解题思路
仍然使用双指针。
从两端开始比较:
- 如果
s[left] === s[right],说明这一组字符没问题,继续向中间移动 - 如果
s[left] !== s[right],说明出现了冲突 - 因为最多只能删除一个字符,所以此时只有两种选择:
- 删除左边字符,继续判断
left + 1到right - 删除右边字符,继续判断
left到right - 1
- 删除左边字符,继续判断
- 只要其中一种情况能形成回文,就返回
true
代码如下:
function validPalindrome(s) {
function isPalindromeRange(left, right) {
while (left < right) {
if (s[left] !== s[right]) {
return false;
}
left++;
right--;
}
return true;
}
let left = 0;
let right = s.length - 1;
while (left < right) {
if (s[left] !== s[right]) {
return (
isPalindromeRange(left + 1, right) ||
isPalindromeRange(left, right - 1)
);
}
left++;
right--;
}
return true;
}
console.log(validPalindrome("aba")); // true
console.log(validPalindrome("abca")); // true
console.log(validPalindrome("abbda")); // true
console.log(validPalindrome("abc")); // false
以 "abca" 为例:
a b c a
^ ^
left right
第一轮 a === a,继续往中间走。
a b c a
^ ^
l r
此时 b !== c,出现冲突。
我们尝试两种情况:
- 跳过
b,判断"c",成立 - 跳过
c,判断"b",也成立
所以 "abca" 最多删除一个字符后可以成为回文字符串。
再看 "abbda":
a b b d a
^ ^
两边 a === a,继续。
a b b d a
^ ^
此时 b !== d,出现冲突。
尝试:
- 跳过左边
b,剩余区间是"bd",不是回文 - 跳过右边
d,剩余区间是"bb",是回文
所以 "abbda" 实际上应该返回 true,删除 d 后得到 "abba"。
如果代码中把 "abbda" 当成 false,那就是示例预期写错了。
复杂度分析:
- 时间复杂度:
O(n) - 空间复杂度:
O(1)
虽然冲突时会调用两次区间回文判断,但它们最多也只是继续扫描剩余区间,所以整体仍然是线性的。
七、完整代码汇总
function reverseString(str) {
return Array.from(str).reverse().join("");
}
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;
}
function validPalindrome(s) {
function isPalindromeRange(left, right) {
while (left < right) {
if (s[left] !== s[right]) {
return false;
}
left++;
right--;
}
return true;
}
let left = 0;
let right = s.length - 1;
while (left < right) {
if (s[left] !== s[right]) {
return (
isPalindromeRange(left + 1, right) ||
isPalindromeRange(left, right - 1)
);
}
left++;
right--;
}
return true;
}
console.log(reverseString("abc")); // "cba"
console.log(isPalindrome("level")); // true
console.log(validPalindrome("abca")); // true
console.log(validPalindrome("abbda")); // true
console.log(validPalindrome("abc")); // false
八、面试表达建议
如果面试官问“怎么判断回文字符串”,可以这样回答:
最直接的方式是把字符串反转后和原字符串比较,时间复杂度是 O(n),空间复杂度也是 O(n)。更优的方式是使用双指针,从左右两端向中间比较,时间复杂度仍然是 O(n),但空间复杂度可以降到 O(1)。
如果面试官继续问“最多删除一个字符后是否能成为回文字符串”,可以这样回答:
还是使用双指针。当左右字符相等时继续向中间移动;第一次遇到不相等时,因为最多只能删除一个字符,所以只需要分别尝试跳过左字符和跳过右字符。只要其中一个剩余区间是回文,就返回 true。
这类题的重点不是 API 背诵,而是要看出字符串的对称性,并用双指针把空间复杂度优化下来。