parseInt()函数绝不是你想的那么简单~~

868 阅读6分钟

首先,我们从一个经典的面试题作为开胃菜,请慢慢品尝。

一个面试题引发的思考

下面这个例子是一道经典的面试题:

['1', '2', '3'].map(parseInt);

相信不少同学第一次碰到这个题目时,非常自然的写出了答案:[1,2,3]

也有一部分同学揣测了一下出题者的意图,既然是面试题,绝不是表面上想的那么简单。

确实如此,这道题不简单,最终结果是 [1, NaN, NaN]

我们分析拆解一下,这道题涉及到了两个知识点:

  • map函数
  • parseInt函数

接下来具体分析下这两个函数

map函数

map函数,相信大家已经再熟悉不过了

map函数可以将一个数组映射为一个新数组。它接收一个callback回调函数作为参数,这个回调函数体现了将原数组映射成新数组的映射关系。

原数组在循环遍历数组每一项时,都会调用一次callback回调函数,并传入三个参数:

  • 当前正在遍历的元素
  • 元素索引
  • 原数组本身 (这个参数基本不使用)

callback函数对当前遍历的元素进行包装执行,得到的返回值就是新数组中对应的结果

const result = ['1', '2', '3'].map((item, index, arr) => item * 2);
// result --> [2, 4, 6]

所以原题其实等同于

['1', '2', '3'].map((item, index, array) => parseInt(item, index, array));

那么接下来就该parseInt出场了

parseInt函数

parseInt() 函数可解析一个字符串,并返回一个整数。

语法格式:parseInt(string, radix)

parseInt接收两个参数:

  • 第一个参数string:要被解析的字符串,如果不是字符串会被转换,忽视空格符
  • 第二个参数radix:要解析的数字的基数。该值介于2 ~ 36之间。默认值为10,表示十进制。这个参数表示将前面的字符从radix进制转化为十进制
      1. 在没有指定基数,或者基数为0的情况下,parseInt()会根据string参数来判断数字的基数。
      • 如果字符串string以"0x"或者"0X"开头, 则基数是16 (16进制).
      • 如果字符串string以"0"开头, 基数是8(八进制)或者10(十进制),那么具体是哪个基数由实现环境决定。ECMAScript 5 规定使用10,但是并不是所有的浏览器都遵循这个规定。因此,永远都要明确给出radix参数的值。
      • 如果字符串string以其它任何值开头,则基数是10 (十进制)。
      1. 如果radix2 ~ 36之外会返回NaN。

我们来看几个栗子🌰:

// 例1
console.log(parseInt(3, 8));  // 3
// 例2
console.log(parseInt(3, 2));  // NaN
// 例3
console.log(parseInt(3, 0));  // 3
// 例4
console.log(parseInt(3, 1));  // NaN
// 例5
console.log(parseInt(123, 5)); // 结果为38

解析如下

  • 例1parseInt里面有两个参数,第二个参数是8,表示要将八进制的3转换为十进制的结果,八进制中有3,转化为十进制还是3,所以返回结果为3
  • 例2parseInt里面有两个参数,第二个参数是2,表示要将二进制的3转化为十进制,额...,不好意思,二进制中并没有3,所以返回NaN
  • 例3parseInt里面有两个参数,第二个参数是0,根据规则1,默认就是十进制,直接返回3
  • 例4parseInt里面有两个参数,第二个参数是1,根据规则2,1在2 ~ 36之外,直接返回NaN
  • 例5parseInt里面有两个参数,第二个参数是5,表示要将五进制的123转化为十进制,结果为38 => (1*5^2 + 2*5^1 + 3*5^0 = 38)

看到这里,我们就可以清楚的理解parseInt的用法了。

接下来我们来分析下原题:

因为parseInt只接收2个参数,因此map的callback函数只把(item:元素值, index:索引)这两个有效参数传给了parseInt,第三个参数无效。

那么这个原题就可以分解为求解下面这三个函数的值,并返回结果

// 实际代码运算等同于
parseInt('1', 0) // 1 (基数为0,此时表示十进制的1,结果还是1)
parseInt('2', 1) // NaN (基数为1,不在`2 ~ 36`范围内,返回NaN)
parseInt('3', 2) // NaN (前面已经分析过了,二进制中没有3,返回NaN)

所以为了避免这个坑,平时写 map 还是不要偷懒了,完整的写法才更直观且更容易维护。

['1', '2', '3'].map(value => parseInt(value));

面试题的变种

除了上面提到的经典面试题,parseInt还衍生除了其他的变种,来看看下面这些栗子🌰

console.log(parseInt(1/0, 19))  // 18
console.log(parseInt(false, 16)) // 250
console.log(parseInt(parseInt, 16)) // 15
console.log(parseInt({}, 16)) // NaN

看到这些奇葩代码,先别懵逼,咱稍微分析分析:

  • console.log(parseInt(1/0, 19))

    解析parseInt里面有两个参数,第二个参数是19,十九进制包括(0,1,2,3,4,5,6,7,8,9,a,b,c,d,e,f,g,h,i);然后分析第一个参数1/0,这货等于Infinity,那么相当于要把Infinity从19进制转化为十进制,我们从第一个字符I开始 -- 19进制有i,表示18,接下来字母n -- 哦豁,19进制没这玩意儿,既然不认识,那就不管了,立即返回i(对应十进制中的18),所以返回结果为18

  • console.log(parseInt(false, 16))

    解析parseInt里面有两个参数,第二个参数16,十六进制包括(0,1,2,3,4,5,6,7,8,9,a,b,c,d,e,f);然后分析第一个参数false,转化为字符串(parseInt第一个参数非字符串先转化为字符串)==> false, 好的,从第一个字符开始分析,f认识,a认识,l哦豁,不认识,立即返回fa (十六进制的fa转换成十进制等于250),结果为250

  • console.log(parseInt(parseInt, 16))

    解析parseInt里面有两个参数,第二个参数是16,十六进制包括(0,1,2,3,4,5,6,7,8,9,a,b,c,d,e,f);然后分析第一个参数parseInt,嗯?怎么还套娃了?先别慌,转化为字符串,我们看看String(parseInt)是个啥, function parseInt() { [native code] }, 好的,从第一个字符开始分析,f认识,u不认识,立即返回f (十六进制的f转换成十进制等于15), 返回结果15

  • console.log(parseInt({}, 16))

    解析:parseInt里面有两个参数,第二个参数是16,十六进制包括(0,1,2,3,4,5,6,7,8,9,a,b,c,d,e,f);然后分析第一个参数{}, 我们看看String({})是个啥,[object Object], 第一个字符是[, 不认识,那就返回NaN

呼,一套打完收工,分析完毕。 经过上面一轮分析,是不是豁然开朗。

最后来总结一下:

  • map函数需要注意回调函数的三个参数涵义,编写代码时尽量写完整,防止掉坑。
  • parseInt第一个参数如果不是字符串,先尝试转换为字符串,再进行进制转化。
  • parseInt第二个参数根据上面的转化规则进行分析即可
  • parseInt第二个参数范围到36,其实是0~9的十进制,加上26个字母,即三十六进制

相信只要掌握了parseInt的转化规则,按照套路来拆解分析,下次再遇到类似的奇葩问题,也能手到擒来。