一、前言
JavaScript 是一门有趣的语言——既支持函数式编程,又是彻底面向对象的语言。
它允许我们在字符串、数字、布尔值上调用方法,比如 "hello".length、(520.1314).toFixed(2)。
在传统语言(如 C 或 Python)里,这看起来是不可思议的,因为“基础类型”在那些语言中并不是对象。
然而 JS 为了让语法统一,底层自动为我们做了“包装” —— 这就是今天要讲的重点之一。
同时,我们还会从常见的 map() 开始,带你理解底层运行原理、NaN 的本质,以及字符串对象的秘密。
二、map() 方法详解
map() 是 ES6 引入的数组方法,用于创建一个新数组,新数组中的每个元素都是原数组元素经过回调函数计算后的结果。
const arr = [1, 2, 3];
const newArr = arr.map(item => item * item);
console.log(newArr); // [1, 4, 9]
语法:
arr.map(callbackFn(currentValue, index, array))
currentValue:当前元素index:当前索引array:原数组
map() 不会修改原数组,返回的是一个全新的数组。
三、一个经典误区:parseInt + map
很多初学者写出过这行代码:
console.log([1, 2, 3].map(parseInt));
很多人以为结果是 [1, 2, 3],
实际上输出是:
[1, NaN, NaN]
为什么?
我们先打印看看:
[1, 2, 3].map(function(item, index, arr) {
console.log(item, index, arr);
return item;
});
结果是:
1 0 [1,2,3]
2 1 [1,2,3]
3 2 [1,2,3]
也就是说,map() 的第二个参数是 index,
而 parseInt 的第二个参数是 进制(radix) !
👉 所以执行的是:
parseInt(1, 0); // 1 (0或空=>按10进制)
parseInt(2, 1); // NaN (进制1非法)
parseInt(3, 2); // NaN (3在二进制中非法)
这就得到了 [1, NaN, NaN]。
总结:
map(parseInt) 的问题不在 map,而在于 parseInt 的参数含义冲突。
写回调时应自己包一层函数:
arr.map(x => parseInt(x));
四、parseInt 的一些陷阱与用法
console.log(parseInt("108")); // 108
console.log(parseInt("八百108")); // NaN(开头非数字)
console.log(parseInt("108八百")); // 108(从头读到非数字为止)
console.log(parseInt("1314.520")); // 1314(小数点被截断)
console.log(parseInt("ff", 16)); // 255(十六进制)
console.log(parseInt("10", 8)); // 8(八进制)
parseInt 的两个核心点:
- 从左向右读取数字字符,直到遇到第一个非法字符为止。
- 第二个参数 radix 表示进制(取值 2~36)。
五、认识 NaN:Not a Number
NaN 是一个特殊的数字类型值,表示“不是一个数字”。
console.log(NaN, typeof NaN); // NaN 'number'
奇怪吧?NaN 居然也是 number 类型!
它来源于无效的数学计算:
console.log(0 / 0); // NaN
console.log(6 / 0); // Infinity
console.log(-6 / 0); // -Infinity
console.log(Math.sqrt(-1)); // NaN
console.log("abc" - 10); // NaN
console.log(undefined + 5); // NaN
console.log(parseInt("hello")); // NaN
在判断 NaN 时不能用 ===:
console.log(NaN === NaN); // false
所以要用:
Number.isNaN(value)
示例:
if (Number.isNaN(parseInt("hello"))) {
console.log("不是一个数字,不能继续计算了");
}
六、JS 的“面向对象一切”理念
在 Python 里你会写:
len("hello")
而在 JS 里你写:
"hello".length
为什么字符串、数字能直接“像对象一样”调用方法?
因为 JS 在底层偷偷帮我们做了这件事 👇:
"hello".length
// 实际等价于
(new String("hello")).length
也就是说,JS 自动把简单数据类型临时“包装”为对象 —— 这就是 包装类(Wrapper Object) 。
三种基本包装类:
| 简单类型 | 包装类 |
|---|---|
| string | String |
| number | Number |
| boolean | Boolean |
let str = "hello"; // 简单数据类型
let strObj = new String("hello"); // 对象类型
console.log(typeof str); // "string"
console.log(typeof strObj); // "object"
包装类只是临时对象,在代码执行完后就会被释放:
var strObj = new String("hello");
console.log(strObj.length);
strObj = null; // 手动释放
七、字符串的底层与编码细节
JS 字符串基于 UTF-16 编码存储。
常规字符用两个字节表示,但像 emoji 或生僻字可能占 两个或更多编码单元。
console.log('a'.length); // 1
console.log('中'.length); // 1
console.log('𝄞'.length); // 2 (emoji / 特殊符号)
再看几个常见操作:
const str = " Hello, 世界! 👋 ";
console.log(str.length); // 包含空格和 emoji
console.log(str[1]); // "H"
console.log(str.charAt(1)); // "H"
console.log(str[1] === str.charAt(1)); // true
console.log(str.slice(1, 6)); // "Hello"
console.log(str.substring(1, 6)); // "Hello"
八、slice 与 substring 的区别
| 特性 | slice() | substring() |
|---|---|---|
| 是否支持负索引 | ✅ 支持 | ❌ 不支持 |
| 参数顺序处理 | 保持原样 | 会自动交换参数 |
| 原字符串是否被修改 | ❌ 不会 | ❌ 不会 |
示例:
let str = "hello";
console.log(str.slice(-3, -1)); // "ll"
console.log(str.substring(-3, 0)); // "" (-3 转换为 0)
console.log(str.slice(3,1)); // "" (从3到1为空)
console.log(str.substring(3,1)); // "el"(自动交换参数)
索引方法也很常用:
console.log(str.indexOf("l")); // 2
console.log(str.indexOf("o")); // 4
console.log(str.lastIndexOf("l")); // 3
九、从 map 到对象:JS 的哲学
JavaScript 的一切几乎都是对象:
- 数组是对象(有方法 map、forEach 等)
- 函数是对象(可以有属性)
- 字符串、数字、布尔值在使用时自动包装为对象
这就是为什么 "hello".length、(3.1415).toFixed(2) 都能正常工作。
而这种“面向对象的一致性”正是 JS 的魅力所在。
十、延伸思考
- 为什么 JS 要让
NaN的类型仍然是number? - 如果我们自己实现一个
myMap(),该怎么写? - JS 为什么自动回收包装对象?
这些问题都能帮你进一步理解语言的设计哲学。
推荐你自己尝试实现:
Array.prototype.myMap = function(fn) {
const res = [];
for (let i = 0; i < this.length; i++) {
res.push(fn(this[i], i, this));
}
return res;
};
然后再配合断点调试,就能更直观地看到回调的执行过程。
JS 的世界充满了“坑”,但理解底层原理后,你会发现这些“坑”其实是语言设计的必然结果。
无论是 map(parseInt) 的 bug,还是 "hello".length 的“魔法”,它们都源于 JS 的统一设计思想:
一切皆对象,一切可被操作。