我昨天写反转字符串的时候,顺手就敲了个 str.reverse(),按下运行的瞬间还觉得这有啥难的。结果控制台直接给我甩了个 TypeError,说 reverse 不是个函数。当时我还愣了两秒 ------ 数组都自带 reverse 方法,字符串凭啥没有?
一行报错引发的思考:字符串为啥不能直接 reverse
说出来有点丢人,我第一反应居然是去搜 "js 字符串 reverse 方法",搜完才反应过来,哦对,JS 里字符串本身真没给 reverse 这个 API。想反转字符串,得借数组的力。
就是大家都眼熟的那套连招:先 split 拆成字符数组,数组 reverse 反转,再 join 拼回字符串。
javascript
const str = 'hello world';
// console.log(str.reverse()); // 报错!字符串根本没这方法,坑了我半分钟
// console.log([1,2,3].reverse()); // 数组就可以,气人不
const res = str.split('').reverse().join('');
console.log(res); // dlrow olleh
写是写出来了,但我心里多了个问号。你说字符串是基本数据类型吧,它不光能调 split,还能调 length、charAt 一堆方法。基本类型不就是存个值吗,哪来的属性和方法?
顺手搞懂了:基本类型为啥能调用方法
说实话这个问题我之前一直稀里糊涂的,就觉得 "能用就行"。那天趁着卡壳,索性翻了翻基础概念,才搞懂包装类这回事。
你就这么想:JS 这门语言从设计之初就想走 "一切皆对象" 的路子,但又不能把字符串、数字这些简单值真的做成对象 ------ 那样太占内存了。怎么办?搞个临时包装。
当你写下 str.length 的时候,JS 底层偷偷干了三件事:
- 临时 new 一个 String 实例,把你的基本类型字符串包成对象
- 用这个对象去访问 length 属性,拿到值返回给你
- 用完立刻把这个临时对象销毁,跟啥都没发生过一样
javascript
// 你写的代码
let str = "hello world"
console.log(str.length)
// 底层偷偷帮你做的事
// let temp = new String("hello world")
// console.log(temp.length)
// temp = null // 用完就扔
冷知识也正因为是临时对象,你没法给基本类型字符串直接加属性。你刚加上,下一行临时对象就没了,再访问肯定是 undefined。别问我怎么知道的,试过。
顺着这个思路,call、apply 这些方法也能串起来了。函数本身也是对象,call 说白了就是 "借方法给别人用",顺便把 this 指过去。比如这段经典代码:
javascript
let o = {
name: '龙老板',
say: function () {
console.log(this.name)
}
}
let obj = {
name: '小龙'
}
o.say(); // 龙老板,this 本来指向 o 本身
o.say.call(obj); // 小龙,把 this 强行改成 obj,函数照样跑
之前总觉得 call 很玄乎,现在想通了 ------ 方法是死的,this 是谁,全看是谁在调用。call 就是帮你手动指定了调用者。
回文字符串:从偷懒写法到双指针
搞懂反转字符串之后,自然就碰到了回文判断。啥是回文?正着读倒着读一模一样,比如 "yessey" 这种。
最偷懒的写法,直接用刚学的反转连招,对比原串和反转串就行:
javascript
// api 解法,写起来最快,面试赶时间可以先写这个
function isPalindrome(str) {
const reversedStr = str.split('').reverse().join('');
return str === reversedStr;
}
这种写法胜在简洁,白板写题一分钟就能搞定。但我写完总觉得有点 "浪费"------ 人家只是让你判断是不是回文,你非得把整个字符串全反转一遍。如果字符串特别长,后半段其实根本不用比。
于是就有了双指针的写法。左右两个指针,一头一尾往中间走,每走一步对比一次字符。只要有一对不一样,直接返回 false;走到中间都一样,那就是回文。
javascript
// 双指针解法,省一半的事
function isPalindrome(str) {
let len = str.length;
// 只走一半就行,走到中间就收手
for (let i = 0; i < len / 2; i++) {
if (str[i] !== str[len - i - 1]) { // 注意这里要减 1,别越界
return false;
}
}
return true;
}
这里我踩过个小坑,len - i - 1 那个减 1 一开始忘了写,直接越界拿了个 undefined,对比半天都是 false,还以为自己逻辑错了。调试了两轮才反应过来 ------ 数组下标从 0 开始啊大哥。
进阶题:删一个字符还能是回文吗
本来以为回文也就这点东西,直到碰到一道变种题:给一个非空字符串,最多删除一个字符,判断能不能变成回文。
我第一反应是暴力解法:遍历每个字符,删掉它,再判断剩下的是不是回文。写完自己都觉得蠢 ------ 字符串长一点,得重复判断好几十遍,效率太低了。
后来想了想,其实不用全遍历。回文是两头对称的,我们还是用左右指针往中间走。只要碰到一对不相等的字符,问题就简化了:要么删掉左边这个,要么删掉右边这个,只要其中一种情况剩下的部分是回文,那就返回 true。
说白了就是 "贪心" 一步:第一次遇到不对称的地方,给一次容错机会,试两种删法,都不行那就真不行了。
javascript
function validPalindrome(s) {
const len = s.length;
let left = 0, right = len - 1;
// 先正常往中间走,碰到不一样的就停
while (left < right && s[left] == s[right]) {
left++;
right--;
}
// 试删左边一个,再判断剩下的区间是不是回文
if (isPalindrome(left + 1, right)) {
return true;
}
// 试删右边一个
if (isPalindrome(left, right - 1)) {
return true;
}
// 内部辅助函数,只判断指定区间,不用截字符串
function isPalindrome(left, right) {
while (left < right) {
if (s[left] !== s[right]) {
return false;
}
left++;
right--;
}
return true;
}
return false;
}
console.log(validPalindrome('abca')); // true,删掉 c 就行
注意这里的坑我一开始把内部的 isPalindrome 写成了接收整个字符串的版本,还要传截取后的子串。后来才反应过来,直接传左右下标就行,根本不用截字符串,省内存还少一步操作。
这道题我当时还纠结过:万一有多处不对称,删一个有用吗?后来想通了,只要有两处不对称,删一个肯定救不回来,所以第一次碰到不对称的时候给一次机会就够了。
几个容易忽略的细节
捋完这几道题,我回头翻了翻笔记,又补了两个之前模糊的点。
一个是怎么准确判断一个值是不是字符串类型。typeof 对基本类型字符串和 new String 出来的对象会返回不同结果,但有时候你需要更精准的区分。这时候就用 Object.prototype.toString.call(),它能返回最准确的内部类型标签。
javascript
let str1 = "abc"; // 基本类型字符串
let str2 = new String("abc"); // 字符串对象
console.log(typeof str1); // string
console.log(typeof str2); // object
console.log(Object.prototype.toString.call(str1)); // [object String]
console.log(Object.prototype.toString.call(str2)); // [object String]
说穿了也简单,每个对象内部都藏着一个类型标签,toString 能把它读出来。数组、日期这些也一样,用这个方法判断最准。
另一个是,别啥场景都硬上双指针。如果字符串本身就很短,比如就十几个字符,直接反转写法反而更省心,代码少还好读。性能优化是要看场景的,为了优化而优化没必要。
最后扯两句
折腾一下午,其实核心收获就仨:
第一,字符串本身没有 reverse,想反转要借数组的力,split + reverse + join 是基本功;
第二,基本类型能调用方法,全靠包装类临时兜底,用完就销毁,别想着给它挂属性;
第三,回文问题双指针是经典思路,删字符的变种题记住 "第一次不匹配时试删左右" 就行。
其实这些都是挺基础的东西,但真的自己踩一遍坑、顺着报错往底层挖一挖,感觉比背十遍结论都牢。之前总觉得包装类是个很虚的概念,自从踩了 "字符串加属性不生效" 的坑之后,反而记得特别清楚。
要是你也踩过类似的坑,或者有更巧的写法,评论区留个言,我也学学。