[译]为什么 ['1', '7', '11'].map(parseInt) 在 Javascript 中返回了 [1, NaN, 3]

850 阅读5分钟

本篇文章采用意译,原文地址

你有没有觉得 Javascript 是有点奇怪的。使用 mapparseInt 试着把字符串数组转化成整型数组,看看会发生什么。打开你的控制台,粘贴下面的代码然后执行。

['1', '7', '11'].map(parseInt);

没有得到 [1,7,11,却得到了 [1, NaN, 3]。这究竟是怎么回事?我们首先需要讨论一些 Javascript 的概念,如果你觉得太长,可以跳到最后看总结。

真值和假值

一个简单的 if-else 语句:

if (true) {
    // this always runs
} else {
    // this never runs
}

在这个例子中,条件为真,执行 if 的语句块,条件为假执行 else 的语句块。这是显而易见的,因为 true 是个布尔值。我们把非布尔值的东西作为条件,会发生什么?

if ("hello world") {
    // 它会运行吗?
    console.log("Condition is truthy");
} else {
    // 它呢?
    console.log("Condition is falsy");
}

在控制台里运行上面的代码,你会发现 if 语句块执行了。因为这个字符串对象是真值。

Javascript 对象要么是真值要么是假值。当在布尔值上下文中,比如 if-else 语句,对象被当做真或者假取决于它们的真实性。这个规则很简单:

除了:false, 0, ""(空字符串),null, undefined, 和 NaN 之外,其他的都是真值。

令人困惑的是,字符串 "false" 和 字符串 "0" 还有空对象 {}, 空数组 [],都是真值。你可以通过给布尔函数传值来进行双向验证,比如 Boolean("0")

我们的目的是,知道 0 是假值就足够了。

基数

0 1 2 3 4 5 6 7 8 9 10

当我们从 0 数到 9 的时候,每个不同的符号代表一个数字。然而到 10 的时候,我们需要两个符号来表示。这是因为我们是十进制系统,它的基数是 10.

基数是用一个以上的符号表示的最小数字。不同的计数系统有着不同的基数。

DECIMAL   BINARY    HEXADECIMAL
RADIX=10  RADIX=2   RADIX=16
0         0         0
1         1         1
2         10        2
3         11        3
4         100       4
5         101       5
6         110       6
7         111       7
8         1000      8
9         1001      9
10        1010      A
11        1011      B
12        1100      C
13        1101      D
14        1110      E
15        1111      F
16        10000     10
17        10001     11

看看上面的表,数字 11 在不同的系统中表示也不同。如果基数是 2,它是 3,如果基数是 16,它是 17.

我们上面的例子中吧 '11' 转换成了 3,就是上面表格二进制的表现。

函数参数

Javascript 中的函数可以传递任意数量的参数,即使跟函数声明的参数不相等。缺少的参数会被当做 undefined,多余的参数会被忽略(但是会存储在类数组的 arguments 对象中)。

function foo(x, y) {
    console.log(x);
    console.log(y);
}
foo(1, 2);      // logs 1, 2
foo(1);         // logs 1, undefined
foo(1, 2, 3);   // logs 1, 2

map()

马上就到我们的重点了!

Map 是 Array 原型上的方法,它返回一个每个原始数组的元素传入函数的结果的新数组。比如下面代码,每个元素在数组中乘 3:

function multiplyBy3(x) {
    return x * 3;
}
const result = [1, 2, 3, 4, 5].map(multiplyBy3);
console.log(result);   // logs [3, 6, 9, 12, 15];

那么现在如果我需要日志输出每个元素,是不是使用 map() 然后传入 console.log 就可以了呢?

[1, 2, 3, 4, 5].map(console.log);

奇怪的事,不只是输出了值,同时把索引和全部数组也输出了。

[1, 2, 3, 4, 5].map(console.log);
// 上面的代码等于
[1, 2, 3, 4, 5].map(
    (val, index, array) => console.log(val, index, array)
);
// 不等于
[1, 2, 3, 4, 5].map(
    val => console.log(val)
);

当给 map() 传入一个方法时,对于每次迭代,都会有三个参数传入方法:currentValue, currentIndex, 和全部 array。这就是为什么每次迭代都会输出全部三个实体内容。

现在离解开我们的谜题越来越近了。

揭晓答案

ParseInt 接受两个参数:stringradix。如果 radix 没有提供,默认值就是 10.

parseInt('11');                => 11
parseInt('11', 2);             => 3
parseInt('11', 16);            => 17
parseInt('11', undefined);     => 11 (没有 radix)
parseInt('11', 0);             => 11 (没有 radix)

现在一步步走进之前的例子:

['1', '7', '11'].map(parseInt);       => [1, NaN, 3]
// 第一次遍历: val = '1', index = 0, array = ['1', '7', '11']
parseInt('1', 0, ['1', '7', '11']);   => 1

因为 0 是假值,所以 radix 基数取值为 10。 因为 parseInt 只接受两个参数,所以第三个 ['1', '7', '11'] 参数被忽略了。

// 第二次遍历: val = '7', index = 1, array = ['1', '7', '11']
parseInt('7', 1, ['1', '7', '11']);   => NaN

因为在基数为 1 的系统中, 7 不存在。同时第三个参数仍然跟第一次迭代一样被省略。所以 parseInt() 返回了 NaN

// 第三次迭代: val = '11', index = 2, array = ['1', '7', '11']
parseInt('11', 2, ['1', '7', '11']);   => 3

基数为 2 的系统中, 符号 11 得出数字 3。最后的参数被省略。

总结

['1', '7', '11'].map(parseInt) 没有像预期那样工作,是因为 map 给 parseInt 传递了三个参数。第二个参数 index 作为 radix 参数传入了 parseInt。所以每个数组的字符串使用了不同的基数来解析。'7' 被解析为基数为 1 的结果,也就是 NaN,11 被解析为基数为 2 的结果,也就是 3。1 被作为默认值解析,因为索引 0 是假值。所以,下面的代码才能正常工作:

['1', '7', '11'].map(numStr => parseInt(numStr));

pic