先记住这个答案
数字自带的 toString 方法接收一个可选参数 radix,表示目标进制。(255).toString(2) 得到 '11111111',传 8 得到 '377',传 16 得到 'ff',不传就是十进制。radix 只能是 2 到 36 之间的整数,因为 0~9 加 a~z 一共 36 个字符,超出范围会抛 RangeError。另外要记住三点:负数只会在结果前面加负号,小数也能转但可能很长,超过 2 的 53 次方的整数本来就存不准,转出来的结果也不可信。
toString的进制范围是 2 到 36- 负数结果是负号加绝对值,不是补码
- 要看补码先
>>> 0再转 - 结果不补 0,定宽输出用
padStart
常用进制和几个边界值
先把最常见的几种写法和边界一起跑一遍,结果都是字符串。
const n = 255
console.log(n.toString(2)) // 二进制
console.log(n.toString(8)) // 八进制
console.log(n.toString(16)) // 十六进制
console.log((10).toString(36)) // 36 进制用到字母 a~z
console.log((-255).toString(16)) // 负数只是前面加负号
console.log((0.5).toString(2)) // 小数也能转
try {
n.toString(37) // radix 只能是 2~36
} catch (e) {
console.log(e.name)
}查看输出与解释
11111111
377
ff
a
-ff
0.1
RangeError字母统一是小写,要大写的话自己再调一次 toUpperCase()。-255 的结果是 '-ff',说明 toString 做的是「符号加绝对值」的转换,和计算机里存负数用的补码不是一回事。0.5 转二进制是 '0.1',因为 0.5 正好是二分之一。换成 0.1 就不一样了,(0.1).toString(2) 会得到一长串 0.000110011001100…,它在二进制里是无限循环小数,只能截断存储。
radix 传小数时会先取整,(255).toString(16.9) 按 16 处理,但这种写法没有意义,面试时知道有这回事就行。
项目里真正会遇到的写法
实际开发里单纯转进制很少出问题,出问题的是位数、补码和大数。比如把颜色分量转成 #rrggbb,(5).toString(16) 只得到 '5',拼出来的颜色值就少了一位。
// 补齐位数:toString 不会自动补 0
console.log((5).toString(2).padStart(8, '0'))
// 看负数的补码:先用 >>> 0 转成 32 位无符号整数
console.log((-5 >>> 0).toString(16))
console.log((-1 >>> 0).toString(2).length)
// 反过来:parseInt 按指定进制解析
console.log(parseInt('ff', 16), parseInt('11111111', 2))
// 超过 2^53 的整数,Number 已经存不准了
console.log((2 ** 53 + 1).toString(16))
console.log((2n ** 53n + 1n).toString(16))查看输出与解释
00000101
fffffffb
32
255 255
20000000000000
20000000000001padStart(8, '0') 是定宽输出的标准写法,颜色值用 padStart(2, '0')。
>>> 0 是无符号右移 0 位,作用是把数字按 32 位整数重新解释成无符号数。-5 的 32 位补码是 0xfffffffb,这样就能在调试位运算时看到真实的位模式。注意它只对 32 位范围有效。
2 ** 53 + 1 在 Number 里存不下,已经被舍成了 2 ** 53,所以十六进制结果末位是 0。这不是 toString 的错,是数字在转换之前就已经不对了。处理 ID、哈希这类大整数时要用 BigInt,BigInt 同样支持 toString(radix)。
- 转换是双向的
toString(radix)负责数字转字符串,反方向用parseInt(str, radix)。两者的进制范围一致,都是 2~36。parseInt遇到不属于该进制的字符会停下,比如parseInt('12', 2)只认第一个1,结果是 1。
容易答错的地方
- 以为负数会转出补码
- 很多人以为
(-5).toString(2)会得到一串 1 开头的二进制,实际结果是'-101'。toString不关心数字在内存里怎么存,只做数学意义上的进制转换。要看补码得先用>>> 0转成无符号整数。 - 直接在整数字面量后面点方法
255.toString(16)会报SyntaxError,因为解析器把255.当成了一个小数。加括号、写两个点,或者放进变量都行。255.5.toString(16)反而能正常运行,因为第一个点已经被小数占用了。- 把结果当数字用
toString返回的永远是字符串。(255).toString(2)得到的'11111111'如果直接参与加法,会变成字符串拼接。需要数值时再用parseInt按原进制解析回来。
面试官还会怎么问?
不用 toString,怎么手写十进制转二进制?
整数部分用「除 2 取余,逆序排列」:不断对 2 取余并把余数放到结果前面,直到商为 0。小数部分用「乘 2 取整,顺序排列」,每次乘 2 取出整数位,剩下的小数继续乘,要设一个最大位数,因为 0.1 这类数永远乘不完。
为什么 radix 最大是 36?
进制需要用字符表示每一位,JavaScript 选的是 0~9 加 26 个英文字母,一共 36 个。短链接、随机 ID 常用 toString(36),就是因为它是内置支持的最紧凑的表示。
字面量能直接写二进制和八进制吗?
可以。ES2015 起 0b1010 是二进制,0o17 是八进制,0xff 是十六进制,它们都在解析时直接变成普通的 Number,和十进制写法没有区别。老式的 017 八进制写法在严格模式下是语法错误,不要再用。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。