本文摘自:《Why ['1', '7', '11'].map(parseInt) returns [1, NaN, 3] in Javascript》
译者:战五渣
JavaScript真的很奇怪,不相信我?那我们尝试用map
和parseInt
将字符串数组转换成整数数组。在chrome上启动控制台(win为F12键,mac为cammand+option+i),复制粘贴下面的代码,然后回车运行。
['1', '7', '11'].map(parseInt);
复制代码
运行以后我们得到的竟然不是一个整数数组[1, 7, 11]
,而是[1, NaN, 3]
。What??为了知道这到底发生了什么,我们首先需要聊聊JavaScript的一些概念。如果你喜欢TLDR(五渣注:TLDR就是too long,don't read“太长不想看了”),我在文章的结尾写了一个总结。
真值 和 假值
这有一个JavaScript的简单的if-else
语句:
if (true) {
// 这总是执行
} else {
// 这永远不会执行
}
复制代码
在这种判断条件一直是true
的情况下,会一直执行if
中的代码(if-block
),而else
中的代码(else-block
)永远不会执行。这很简单是不是,就是因为true
就是一个布尔值,那如果我们将一个非布尔值的值作为条件会是什么样的呢??
if ('hello world') {
// 这还会运行吗?
console.log('判断条件是真')
} else {
// 还是运行这里?
console.log('判断条件是假')
}
复制代码
让我们在控制台中运行这段代码。
我们能发现这种情况下,实际运行的是if-block
,这是因为字符串hello world
为真值(五渣注:因为js会进行‘隐式转换’)。
每个JavaScript对象不是真值就是假值。当放置在布尔型上下文(例如if-else语句)中时,根据对象的真实性(也就是进行隐式转换)将其视为true
或者false
。那什么值会被判断为真值,什么值会被判断为假值呢?遵循下面这个简单的规则:
除了后面这些值外,其他的值都会被判断为真值:false
、0
、''
(空字符串)、null
、undefined
和NaN
这就很让人困惑了,这就意味着'false'
字符串false,'0'
字符串0,一个空对象{}
和一个空数组[]
都是真值,咱可以通过把这些不确定的对象传给Boolean函数(例如Boolean('0')
)来看看是不是真如上面所说的。
就我们现在而言,只要知道0
会判断为false
就行了
基数
0 1 2 3 4 5 6 7 8 9 10
复制代码
当我们从0数到10的时候,每个数(0-9)都是都是一个单独不同的数字,但是,一旦数到10,就需要两个数字(1和0)来表示这个数。这是因为我们十进制的基数为十,逢十进一。
基数用一个以上的数字表示的最小的数。不同的进制具有不同的基数,因此,相同的数字在不同的进制中可以表示不同的大小。
十进制 二进制 十六进制
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在不同的进制中表示不同的数字。在二进制中的11,对应的十进制为3。在十六进制中的11,则对应的十进制为17。
小伙伴们可能已经注意到了,在我们标题中,当输入11时parseInt返回3,它对应于表上的二进制,还是不懂的话,我们继续往下看。
函数参数
在JavaScript中,可以在调用函数的时候传入任意数量的参数,即使传入的的参数数量跟函数声明时的参数数量不同,也会按照缺少的参数将被视为未定义,多余的参数将被忽略来执行(但是所有的参数都存储在类数组arguments
对象中)
function foo(x, y) {
console.log(x)
console.log(y)
}
foo(1, 2); // 1, 2
foo(1); // 1, undefined
foo(1, 2, 3); // 1, 2
复制代码
Array.prototype.map()方法
我们马上就要讲到重点了!
map()
方法是Array.prototype
上的一个方法,该方法返回一个新数组,其结果是该数组中的每个元素都调用一个提供的函数后返回的结果。例如,以下代码将数组中的每个元素都乘以3:
function multiplyBy3(x) {
return x * 3
}
const result = [1, 2, 3, 4, 5].map(multiplyBy3);
console.log(result); // [3, 6, 9, 12, 15]
复制代码
现在如果我们要打印出数组中的每个元素,那我们应该往map()
函数中传入console.log
,对吧?!
[1, 2, 3, 4, 5].map(console.log)
复制代码
见证奇迹的时刻,每个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
三个参数传给这个函数。这就是为什么console.log
每条输出都是三个值的原因。
现在我们了解了解开title这个谜题的所有条件!
Mix这些条件
parseInt
接受两个参数string
和radix
,如果传入的radix
参数是不正确的,则默认情况下,radix
会被设置为10,也就是默认是十进制。
parseInt('11'); // 11
parseInt('11', 2); // 3
parseInt('11', 16); // 17
parseInt('11', undefined); // 11(基数不正确)
parseInt('11', 0); // 11(基数不正确)
复制代码
现在我们来一步一步运行我们的示例
['1', '7', '11'].map(parseInt); // [1, NaN, 3]
// 第一次迭代,传入参数为 val = '1', index = 0, array = ['1', '7', '11']
parseInt('1', 0, ['1', '7', '11']); // 1
复制代码
第二个参数是基数,传入的0是假值,所以被设置成默认的十进制。因为parseInt
只接受两个参数,所以第三个整个数组的这个参数被忽略掉了。所以'1'
基数10中的字符串对应数字1。
// 第二次迭代,出入参数为 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,最后一个参数被忽略
总结(TLDR:too long, don't read)
['1', '7', '11'].map(parseInt)
没有像我们想象中的那样输出,因为在每次迭代中都会向map
中的parseInt
传递三个参数,第二个参数index
作为radix
参数传递到了parseInt
中。因此,使用了不同的进制解析数组中的每个字符串'11'
被解析为基数为2即二进制,所以返回3。'7'
被解析为基数为1,所以返回NaN
,'1'
的索引是0,0是个假值,所以被重置为十进制,所以还是返回1。
所以我们如果想按照我们的想法运行的话,我们得这么写
['1', '7', '11'].map(numStr => parseInt(numStr));
// 传进去一个参数,就ok了
复制代码
我是前端战五渣,一个前端界的小学生。