原文:https://pantao.parcmg.com/press/parseint-mystery.html
Javascript 总是以超自然的方式执行我们的代码,这是一件很神奇的事情,如果不信的话,思考一下 ['1', '7', '11'].map(parseInt)
的结果是什么?你以为会是 [1, 7, 11]
吗?我都这么问了,那肯定不是:
['1', '7', '11'].map(parseInt)
// 输出:(3) [1, NaN, 3]
要理解为什么为会这里,首先需要了解一些 Javascript 概念,如果你是一个不太喜欢阅读长文的人,那直接跳到最后看结论吧
真值与假值
请看下面示例:
if (true) {
// 这里将启动都会被执行
} else {
// 这里永远都不会被执行
}
在上面的示例中, if-else
声明为 true
,所以 if-block
会一直都执行,而 else-block
永远都会被忽略掉,这是个很容易理解的示例,下面我们来看看另一个例子:
if ("hello world") {
// 这里会被执行吗?
console.log("条件判断为真");
} else {
// 或者会执行这里吗?
console.log("条件判断为假");
}
打开你的开发者工具 console
面板,执行上面的代码,你会得到看到会打印出 条件判断为真,也就是说 "hello world"
这一个字符串被认为是真值。
在 JavaScript 中,truthy(真值)指的是在布尔值上下文中,转换后的值为真的值。所有值都是真值,除非它们被定义为 假值(即除 false、0、""、null、undefined 和 NaN 以外皆为真值)。引用自:Mozilla Developer: Truthy(真值)
这里一定要划重点:在布尔上下文中,除 false
、0
、""
(空字符串)、null
、undefined
以及 NaN
外,其它所有值都为真值
基数
0 1 2 3 4 5 6 7 8 9 10
当我们从 0
数到 9
,每一个数字的表示符号都是不一样的(0-9),但是当我们数到 10
的时候,我们就需要两个不同的符号 1
与 0
来表示这个值,这是因为,我们的数学计数系统是一个十进制的。
基数,是一个进制系统下,能使用仅仅超过一个符号表示的数字的最小值,不同的计数进制有不同的基数,所以,同一个数字在不同的计数体系下,表示的真实数据可能并不一样,我们来看一下下面这张在十进制、二进制以及十六进制不同值在具体表示方法:
十进制(基数:10) | 二进制(基数:2) | 十六进制(基数: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 |
18 | 10002 | 12 |
你应该已经注意到了, 11 在上表中,总共出现了三次。
函数的参数
在 Javascript 中,一个函数可以传递任何多个数量的参数,即使调用时传递的数量与定义时的数量不一致。缺失的参数会以 undefined
作为实际值传递给函数体,然后多余的参数会直接被忽略掉(但是你还是可以在函数体内通过一个类数组对象 arguments
访问到)。
function sum(a, b) {
console.log(a);
console.log(b);
}
sum(1, 2); // 输出:1, 2
sum(1); // 输出:1, undefined
sum(1, 2, 3); // 输出:1, 2
map()
快要有结果了。
map()
是一个存在于数组原型链上的方法,它将其数组实例中的每一个元素,传递给它的第一个参数(在本文最开始的例子中就是 parseInt
),然后将每一个返回值都保存到同一个新的数组中,遍历完所有元素之后,将新的包含了所有结果的数组返回。
function multiplyBy3 (number) {
return number * 3;
}
const result = [1, 2, 3, 4, 5].map(multiplyBy3);
console.log(result); // 输出:[3, 6, 9, 12, 15]
现在,假想一下,我们想使用 map()
将一个数组中的每一个元素都打印到控制台,我可以直接将 console.log
函数传递给 map()
方法:
[1, 2, 3, 4, 5].map(console.log);
输出:
1 0 (5) [1, 2, 3, 4, 5]
2 1 (5) [1, 2, 3, 4, 5]
3 2 (5) [1, 2, 3, 4, 5]
4 3 (5) [1, 2, 3, 4, 5]
5 4 (5) [1, 2, 3, 4, 5]
是不是感觉有点神奇了?为什么会这样?来分析一下上面输出的内容都有些什么?
- 第一列:这个看上去跟我们想要的输出是一致的
- 第二列:这个是一个从
0
开始递增的数字 - 第三列:总是
(5) [1, 2, 3, 4, 5]
再来看看下面这段代码:
[1, 2, 3, 4, 5].map((value, index, array) => console.log(value, index, array));
在控制台上试试执行上面这行代码,你会发现,它与 [1, 2, 3, 4, 5].map(console.log);
输出的结果是完全一致的,**总是会被我们忽略的一点是,map()
方法会将三个参数传递传递给它的函数,分别是 currentValue
(当前值)、currentIndex
(当前索引)以及 array
本身,这就是,上面为什么结果有三列的原因,如果我们只是想将每一个值打印,那么需要像下面这样写:
[1, 2, 3, 4, 5].map((value) => console.log(value));
回到最开始的问题
现在让我们来回顾一下本文最开始的问题:
为什么 ['1', '7', '11'].map(parseInt)
的结果是 [1, NaN, 3]
?
来看看 parseInt()
是一个什么样的函数:
parseInt(string, radix)
将一个字符串string
转换为radix
进制的整数,radix
为介于2-36
之间的数。详情:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/parseInt
这里还有一条需要注意的是,虽然我们一般都是使用 parseInt('11')
这样的调用方式,认为是将 11
按十进制转换成数字,但是,请在使用的时候,永远都添加 radix
参数,因为, 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', '7', '11'].map((value, index, array) => parseInt(value, index, array))
根据前面的知识,我们应该知道, parseInt(value, index, array)
中的 array
会被忽略(并放入 arguments
对象中。
那么,完整的代码就是下面这样的:
['1', '7', '11'].map((value, index, array) => parseInt(value, index))
-
遍历到第一个元素时:
parseInt('1', 0);
radix
为0
,是一个假值,在我们的控制台中,一般将使用10
进制,所有,得到结果1
-
遍历到第二个元素时:
parseInt('7', 1)
在一个
1
进制系统中,7
是不存在的,所以得到结果NaN
(不是一个数字) -
遍历到第三个元素时:
parseInt('11', 2)
在一个
2
进制系统中,11
就是进十制的3
总结
['1', '7', '11'].map(parseInt)
的结果是 [1, NaN, 3]
的原因是因为,map()
方法是向传递给他的函数中传递三个参数,分别为当前值,当前索引以及整个数组,而 parseInt
函数接收两个参数:需要转换的字符串,以及进制基数,所以,整个语句可以写作:['1', '7', '11'].map((value, index, array) => parseInt(value, index, array))
,array
被 parseInt
舍弃之后,得到的结果分别是:parseInt('1', 0)
、parseInt('7', 1)
以及 parseInt('11', 2)
,也就是上面看到的 [1, NaN, 3]
。
正确的写法应该是:
['1', '7', '11'].map(numStr => parseInt(numStr, 10));