很多人学 JS,是从"能把需求写出来"开始的。
但真正在面试或者写复杂业务时,常见的几个小坑------数组的 map、NaN 与 Infinity、字符串的包装类和长度问题------经常一起出来"围殴"你。
这篇文章不讲大而全,只用几组小实验,把它们串成一条线:
从数组遍历,到数字解析,再到字符串与 emoji 的长度。
一、数组不只是 for,map 才是更现代的写法
最传统的遍历数组,往往是这样:
javascript
const arr = [1, 2, 3, 4, 5, 6];
const result = [];
for (let i = 0; i < arr.length; i++) {
result.push(arr[i] * arr[i]);
}
console.log(result); // [1, 4, 9, 16, 25, 36]
而在更现代的写法里,我们会让数组自带的方法来负责"遍历 + 映射":
javascript
const arr = [1, 2, 3, 4, 5, 6];
console.log(arr.map(item => item * item));
// [1, 4, 9, 16, 25, 36]
特点:
- 原数组不变,返回一个新数组
- 更符合"声明式"的风格:告诉它"要做什么",而不是"怎么做"
理解 map 的回调函数签名也很重要:
javascript
array.map(function (item, index, arr) {
// item 当前元素
// index 当前索引
// arr 整个原数组
});
只要记住这三个参数的顺序,你在后面就能看懂更多"骚操作"。
二、当 map 遇上数字解析:NaN 是怎么溜进来的?
有了上面的铺垫,再看这一类代码就不陌生了:
javascript
const arr = [1, 2, 3];
arr.map(function (item, index, source) {
console.log(item, index, source);
return item;
});
这一段可以帮助你记住参数顺序。
然后,在实际项目或面试题中,很容易有人写出类似:
javascript
[1, 2, 3].map(parseInt);
这个例子本身已经很有名了,这里只强调两点:
- 回调函数拿到的是
(item, index, arr) - 数字解析函数的签名是
(string, radix):要解析的字符串 + 进制
当两边的参数顺序撞在一起时,就有了进制被错误传入 的问题,于是 NaN 出现了。
关键不是记住"这题的答案",
而是要记住:数组方法的回调长什么样,别乱用现成函数往上一丢就图省事。
三、NaN 和 Infinity:数字类型里的"异类"
在处理数字时,还有两个非常容易被忽视的存在:
javascript
console.log(0 / 0); // NaN
console.log(6 / 0); // Infinity
console.log(-6 / 0); // -Infinity
以及:
javascript
console.log(parseInt("108")); // 108
console.log(parseInt("八百108")); // NaN
console.log(parseInt("108八百")); // 108
console.log(parseInt(1314.520)); // 1314
可以总结出几个有用的直觉:
-
NaN:表示"这不是一个合法的数字结果",典型场景是:
- 0 / 0
- 完全看不懂的字符串解析(比如一上来就是汉字)
-
Infinity/-Infinity:表示正负无穷大,例如除以 0。
-
parseInt的解析习性:- 从左往右看,一开始就看不懂 → 整体
NaN - 先看懂了一段,中途遇到不认识的字符 → 前面合法的部分照算
- 遇到小数 → 小数点后面直接不要
- 从左往右看,一开始就看不懂 → 整体
再加上一句经典但反直觉的事实:
javascript
typeof NaN === "number"; // true
这就是为什么很多 JS 教程会专门开一小节来讲"特殊数字类型"。
四、看起来"一切皆对象",其实 JS 在背后帮你擦屁股
有一个常被忽略、但又极其常用的能力:
javascript
"hello".length; // 5
(520.1314).toFixed(2); // "520.13"
从传统面向对象的思路看:
- 字符串字面量、数字字面量都属于原始值
- 原始值照理说不是对象,不能随便点属性、调方法
但在这门语言里,你天天在这么写,而且完全没报错。
原因是引擎偷偷给你做了**"包装"**:
- 原始字符串 → 临时变成
String对象 - 原始数字 → 临时变成
Number对象 - 原始布尔值 → 临时变成
Boolean对象
可以用一组简化版的伪操作来理解:
javascript
var str = "hello";
str.length;
// 底层会做类似下面的事:
var strObj = new String(str);
console.log(strObj.length); // 5
strObj = null; // 用完扔掉
console.log(typeof str); // "string"(原始类型没改变)
也就是说:
- 表面上是"统一风格,一切皆能点属性、调方法"
- 实际上是引擎在背后帮你 new 来 new 去
这也解释了为什么这门语言经常被说"很傻瓜化":
为了让你写起来爽,它会帮你兜很多底。
五、字符串长度与 emoji:肉眼看到的"一个"不等于 length === 1
再来看一组和字符串相关的实验:
arduino
js
console.log("a".length); // 1
console.log("中".length); // 1
console.log("𝄞".length); // 2(看起来一个符号,却占了两个长度单位)
在这门语言里,字符串底层使用 UTF-16 编码:
- 大部分常见字符用 一个 16 位单元 表示 →
length加 1 - 某些生僻字和 emoji 用 两个 16 位单元 表示 →
length加 2
再配合一段稍微综合一点的示例:
javascript
const str = " Hello, 世界! 👋 ";
console.log(str.length); // 包含空格、中文、emoji 在内的长度
console.log(str[1]); // 访问第二个 UTF-16 单元
console.log(str.charAt(1), str.charAt(1) == str[1]); // 在常规字符上二者表现一致
console.log(str.slice(1, 6)); // 截取 [1, 6) 区间
console.log(str.substring(1, 6)); // 在这个用法下和 slice 表现一样
```](cascade:incomplete-link)
你可以得到两个非常有用的结论:
length表示的是UTF-16 单元数量,不是"肉眼看到的字符个数"- 对英文、常见汉字,大多数时候可以"假装没区别"
- 一旦大量使用 emoji 或特殊字符,索引和截取就可能和视觉表现错位
在做以下需求时,一定要记住这一点:
- 限制"输入最多 N 个字符"
- 截断字符串用于展示(例如列表项缩略显示)
- 按"字符数"计费、统计、对齐
否则,emoji 往往会成为你 UI 中最顽皮的那一块。
六、把这些零散知识点连成一条线
回头看看前面的几个小实验,它们其实在回答同一类问题:
"这门语言,为了让你写起来看起来简单,到底在底层帮你做了多少事?"
- 数组方法 :
map不只是简化了 for 循环,还规定了固定的回调参数顺序,
一不留神用错现成函数,就会引入莫名其妙的NaN。 - 数字解析与特殊值 :
parseInt的解析规则、NaN和Infinity的存在,都在提醒你:
"看起来是数字,其实里面有很多状态要区分"。 - 包装类 :
"hello".length、(520.1314).toFixed(2)这种写法之所以成立,
是因为底层在帮你悄悄构造临时对象。 - 字符串与编码 :
length和字符视觉上的"个数"不总是对得上的,
emoji 是检验你有没有意识到这一点的最好测试用例。
当你愿意停下来,用几分钟时间亲手敲一遍这些代码,
并且追问每一行输出背后的"为什么",
你就已经不只是"会写这门语言",
而是在慢慢"理解这门语言"。