面试必考!JavaScript 字符串算法从入门到进阶
在 JavaScript 面试中,字符串算法几乎是“必考题”,尤其是反转、回文判断这类看似简单却暗藏玄机的问题。今天,我们就从一道经典题目出发,由浅入深地拆解其中的思路、优化与底层原理,让你读完就能从容应对面试官的连环追问。
算法题的魅力不在于背答案,而在于从暴力到优雅的思考跃迁。
一、反转字符串:为何字符串没有 reverse?
先看一个最基础的问题:如何反转一个字符串?
const str = 'abc';
// 我们希望得到 'cba'
也许你会下意识想到 str.reverse(),但运行会报错 —— 字符串并没有 reverse 方法。
1.1 数组的 reverse 与字符串的间接反转
数组有 reverse 方法,所以我们常用的“曲线救国”方式就是:
const reversed = str.split('').reverse().join('');
console.log(reversed); // 'cba'
三步走:split('') 转成字符数组 → reverse() 反转数组 → join('') 拼回字符串。
1.2 为什么字符串不直接提供 reverse?
这背后其实是 JavaScript 的设计哲学:字符串是不可变的(immutable),而 reverse 会修改原数组。如果字符串上直接提供 reverse,要么破坏不可变性,要么就得返回新字符串,这并非不可能,但语言设计者选择将“可变操作”留给数组,字符串保持简单。
1.3 再探:基本类型如何拥有方法?
你可能会疑惑:明明 str 是基本类型(string),为什么能调用 split 和 join 这些看起来像对象的方法?
这里藏着 JS 的“包装类”机制:
let str = 'abc';
console.log(str.length); // 3
当访问基本类型的属性或方法时,JS 会临时用 new String(str) 将其包装成一个 String 对象,调用完方法后,这个临时对象就被销毁了。这就像灰姑娘的水晶鞋 —— 只在需要时魔法附身,用完即恢复素颜。
JS 的包装类让基本类型“假装”是对象,既保持了简单,又享受了面向对象的便利。
二、回文字符串判断:从暴力到双指针
回文字符串是指正读和反读都一样的字符串,例如 'yessey'、'racecar'。
2.1 暴力法:反转后比较
有了反转字符串的方法,判断回文就很容易了:
function isPalindrome(str) {
const reversed = str.split('').reverse().join('');
return reversed === str;
}
但这种方式会额外创建数组和新字符串,空间复杂度 O(n),且性能不是最优。
2.2 双指针法:边走边比
更高效的方式是利用对称性,从两端向中间比较:
function isPalindrome(str) {
const len = str.length;
for (let i = 0; i < len / 2; i++) {
if (str[i] !== str[len - 1 - i]) {
return false;
}
}
return true;
}
-
时间复杂度 O(n),空间复杂度 O(1)。
-
只需比较前一半与后一半的对应字符,一旦发现不等立即返回
false。
面试时,这一解法足以让面试官点头。
三、衍生问题:最多删除一个字符,能否成为回文?
这是回文字符串的经典升级版:
给定一个非空字符串
s,最多删除一个字符。判断是否能成为回文字符串。
例如 'abca',删除 'b' 或 'c' 后得到 'aca' 或 'aba',都是回文,返回 true。
3.1 思路拆解
我们还是用双指针,但遇到不匹配的字符时,不能直接返回 false,因为有机会删除一个字符。此时有两条路可走:
-
删除左指针指向的字符,检查剩余子串
[i+1, j]是否为回文; -
删除右指针指向的字符,检查剩余子串
[i, j-1]是否为回文。
只要其中一条路能走通,就返回 true。
3.2 代码实现
function validPalindrome(s) {
const len = s.length;
let i = 0, j = len - 1;
// 先正常双指针比较,直到遇到不匹配
while (i < j && s[i] === s[j]) {
i++;
j--;
}
// 辅助函数:检查 s 的 [left, right] 区间是否为回文
function isPalindromeRange(left, right) {
while (left < right) {
if (s[left] !== s[right]) {
return false;
}
left++;
right--;
}
return true;
}
// 尝试删除左指针或右指针的字符
return isPalindromeRange(i + 1, j) || isPalindromeRange(i, j - 1);
}
测试一下:
console.log(validPalindrome('abca')); // true
console.log(validPalindrome('abc')); // false
console.log(validPalindrome('deeee')); // true(删除 'd')
3.3 复杂度与细节
-
时间复杂度:O(n),最多扫描两遍。
-
空间复杂度:O(1),只用了常数级变量。
-
注意
isPalindromeRange的边界,当删除左字符时,区间为[i+1, j];删除右字符时为[i, j-1]。若原字符串本身是回文,while 循环会一直走到 i>=j,然后进入返回语句,此时两个子区间检查都会返回 true(因为区间无效,循环直接跳过),所以结果正确。
遇到“最多删除一个”的题目,牢记“分而治之”——把问题拆成两个子回文判断,再取或。
四、延伸:从 toString 到 stringify 的底层闲谈
在用户的笔记中,还提到了 Object.prototype.toString 和 JSON.stringify,这虽然和题目不直接相关,但却是 JS 进阶面试的常见考点。我们可以稍作延伸。
4.1 toString 与类型判断
let o = { a: 1 };
o.toString(); // "[object Object]"
[1,2,3].toString(); // "1,2,3"
为什么数组的 toString 结果不一样?因为数组重写了 toString 方法。而如果我们想获取精确的类型,可以用:
Object.prototype.toString.call(o); // "[object Object]"
Object.prototype.toString.call([]); // "[object Array]"
这里 call 的作用是借用 Object.prototype.toString 方法,并指定 this 为我们要判断的对象,从而得到准确的类型标签。
4.2 call 的妙用:函数借调
再看一个例子:
let o = {
name: 't老板',
say: function() {
console.log(this.name);
}
};
let obj = { name: 't总' };
o.say.call(obj); // 't总'
call 的第一个参数可以改变函数内部的 this 指向,让 say 方法借用给 obj 使用。
这种借调思想在 JS 中非常常见,比如类数组借用数组方法:
Array.prototype.slice.call(arguments);
4.3 重头戏:Object.prototype.toString 与 JSON.stringify 的本质区别
在笔记中你看到了 JSON.stringify,这里必须把它们拎出来讲透,因为太多中级开发者在这里翻车。
① Object.prototype.toString 的核心是“辨型”
它的设计初衷不是为了给对象“化妆”成好看的字符串,而是为了返回对象的内部属性 [[Class]]。这是 JS 底层用来区分对象“家族血统”的终极手段。
为什么我们总用 Object.prototype.toString.call(obj) 而不是直接 obj.toString()?因为绝大多数内置对象(如 Array、Date、Function)都重写了 toString,直接调用拿不到 [object XXX] 格式。借助 call 强行调用原型上的“老祖宗”方法,才能精准判断:
Object.prototype.toString.call(null); // '[object Null]'
Object.prototype.toString.call(undefined); // '[object Undefined]'
Object.prototype.toString.call(new Date()); // '[object Date]'
笔试小技巧:
typeof null === 'object'是个历史遗留坑,但Object.prototype.toString.call(null)永远不会骗你,它是业界判断数据类型的“金标准”。
② JSON.stringify 的核心是“序列化”
它的目标非常纯粹:把 JS 对象/值转换成符合 JSON 标准的字符串,用于数据传输(比如传给后端)或深拷贝。
它和 toString 最大的区别在于:
-
toString侧重于可读性和类型标识,即使重写了,通常也只是给人看的; -
stringify侧重于数据完整性和格式规范,它会自动忽略函数、undefined、Symbol,因为这些不是合法的 JSON 值。
看个对比例子你就全明白了:
let obj = {
name: '掘金',
age: 10,
say: function() { console.log('hi'); },
hobby: undefined
};
// 1. toString 只是随便打个标记
console.log(obj.toString()); // '[object Object]'
// 2. JSON.stringify 认真做数据序列化
console.log(JSON.stringify(obj)); // '{"name":"掘金","age":10}' (函数和 undefined 被静默忽略)
说白了就是:toString 是 JS 内部用来“贴标签”和“简单展示”的工具方法;而 JSON.stringify 是独立存在的全局 API,负责“数据打包”,两者各有各的赛道,面试时千万别混为一谈。
五、总结与思考
今天我们从一个反转字符串的问题出发,逐步深入到回文判断及其衍生问题,并顺带回顾了 JS 的包装类、toString 和 call 等底层机制。
知识点回顾:
-
字符串不可变,反转需通过数组中转。
-
双指针是判断回文的最优解法,空间 O(1)。
-
“最多删除一个字符”问题,拆解为两个子回文判断,用辅助函数解决。
-
JS 的包装类让基本类型能调用方法,而
call/apply则是改变this的利器。 -
Object.prototype.toString.call是类型检测的“照妖镜”,JSON.stringify是数据序列化的“搬运工”,两者的底层哲学完全不同。
最后送大家一句话:
算法不止是代码,更是思维体操。把每个边界条件想清楚,比背一百道题更有价值。
希望这篇文章能帮你理清思路,下次面试遇到字符串题,信手拈来。
如果你有任何疑问或想深入探讨,欢迎在评论区留言,我们一起交流!
参考代码(汇总)
// 反转字符串
function reverseStr(str) {
return str.split('').reverse().join('');
}
// 双指针判断回文
function isPalindrome(str) {
let i = 0, j = str.length - 1;
while (i < j) {
if (str[i] !== str[j]) return false;
i++;
j--;
}
return true;
}
// 最多删除一个字符
function validPalindrome(s) {
let i = 0, j = s.length - 1;
while (i < j && s[i] === s[j]) {
i++;
j--;
}
function isPal(l, r) {
while (l < r) {
if (s[l] !== s[r]) return false;
l++;
r--;
}
return true;
}
return isPal(i + 1, j) || isPal(i, j - 1);
}
更多推荐



所有评论(0)