JS 一切皆对象?从 map() 到 NaN 带你看清真相

50 阅读5分钟

一、前言

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 的两个核心点:

  1. 从左向右读取数字字符,直到遇到第一个非法字符为止。
  2. 第二个参数 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)

三种基本包装类:

简单类型包装类
stringString
numberNumber
booleanBoolean
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"

八、slicesubstring 的区别

特性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));  // "" (从31为空)
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 的统一设计思想:
一切皆对象,一切可被操作。