JavaScript中的多種進制與進制轉換
- 2021 年 11 月 30 日
- 筆記
- javascript
進制介紹
JavaScript
中提供的進制表示方法有四種:十進制、二進制、十六進制、八進制。
對於數值字面量,主要使用不同的前綴來區分:
- 十進制(Decimal):
取值數字0-9
;不用前綴。 - 二進制(Binary):
取值數字0
和1
;前綴0b
或0B
。 - 十六進制(Hexadecimal):
取值數字0-9
和a-f
;前綴0x
或0X
。 - 八進制(Octal):
取值數字0-7
;前綴0o
或0O
(ES6規定)。
需要注意的是,非嚴格模式下瀏覽器支持:如果有前綴0並且後面只用到
0-7
八個數字的數值時,該數值視為八進制;但如果前綴0後面跟隨的數字中有8或者9,則視為十進制。
嚴格模式下,如果數字加前綴0,則報錯:Uncaught SyntaxError: Decimals with leading zeros are not allowed in strict mode。
各進制的數值,如果取值數字超過給定的範圍,則會報錯:Uncaught SyntaxError: Invalid or unexpected token。
在JavaScript內部的默認情況下,二進制、十六進制、八進制字面量數值,都會自動轉為十進制進行運算。
0x22 // 34
0b111 // 7
0o33 // 27
0x22 + 0b111 // 41
0o33 + 12 // 39
(0x33).toString() // 51
(0x33).valueOf() // 51
除了十進制是Javascript默認的數字進制以外,其他三種進制方式平時使用較少,主要在處理底層數據、位元組編碼或者位運算等時候才會碰到。
進制轉換
本文將主要討論進制轉換時的問題。
JavaScript 提供了原生函數,進行十進制與其他各進制之間的相互轉換。
其中,從其他進制轉換成十進制,有三種方式:parseInt()
,Number()
,+
(一元運算符)。這三種方式都只能轉換整數。
從十進制轉換成其他進制,可以使用 Number.prototype.toString()
。支持小數。
parseInt(str, radix)
第一個參數是需要解析的字符串;其他進制不加前綴。
第二個參數是一個進制基數,表示轉換時按什麼進制來理解這個字符串,默認值10,表示轉十進制。
第二個參數如果非數字,則自動轉數字,如無法轉稱數字則忽略該參數;是數字時,必須是 2-36
的整數,超出該範圍,返回 NaN
。
parseInt('1111', 2) // 15
parseInt('1234', 8) // 668
parseInt('18af', 16) // 6319
parseInt('1111') // 1111
如果不傳入第二參數,則 parseInt
會默認使用十進制來解析字符串;但是,如果字符串以 0x
開頭,會被認為是十六進制數。
而其他進制的字符串,0o21(八進制)
,0b11(二進制)
不會以該進制基數自動轉換,而是得到 0
。
所以,在使用 parseInt
進行進制轉換時,為了保證運行結果的正確性和穩定性,第二個參數不能省略。
parseInt('0x21') // 33
parseInt('0o21') // 0
parseInt('0b11') // 0
parseInt('111', 'add') // 111
parseInt('111', '787') // NaN
如果需要解析的字符串中存在對於當前進制基數無效的字符,則會從最高位取有效字符進行轉換,沒有效字符則返回NaN。
parseInt('88kk', 16) // 136,=== 0x88
parseInt('kk', 16) // NaN
Number()
可以把字符串轉為數字,支持其他進制的字符串,默認轉成十進制數字。
字符串中如果存在無效的進制字符時,返回 NaN
。
記住,需要使用進制前綴,0b
,0o
,0x
。
Number('0b11100') // 28
Number('0o33') // 27
Number('0x33') //51
Number('0x88kk') // NaN
+(一元運算符)
與 Number()
一樣,可以把字符串轉為數字,支持其他進制的字符串,默認轉成十進制數字。
字符串中如果存在無效的進制字符時,返回 NaN
。
也需要使用進制前綴。
+'0b11100' // 28
+'0o33' // 27
+'0x33' //51
+'0x88kk' // NaN
可以看到,基本和 Number()
是一樣的,都在本質上是對數字的一種轉換處理。
Number.prototype.toString(radix)
它支持傳入一個進制基數,用於將數字轉換成對應進制的字符串,它支持轉換小數。
未指定默認值為 10
,基數參數的範圍 2-36
,超過範圍,報錯:RangeError。
15..toString(2) // 1111
585..toString(8) // 1111
4369..toString(16) // 1111
(11.25).toString(2) // 1011.01
自定義轉換
除了這些原生函數以外,也可以自己實現進制數字之間的轉換函數。
根據相應的規則,就可以實現十進制與二進制、十六進制之間的轉換的一些方法。
十進制與十六進制轉換
以下代碼是針對整數在十進制與十六進制之間的轉換,根據基本規則進行換算。
十六進制是以 0-9
、a-f
進行描述數字的一種方式,其中 0-9
取本身數字的值,而 a-f
則取 10-15
的值。
且字母不區分大小寫。
function int2Hex (num = 0) {
if (num === 0) {
return '0'
}
const HEXS = '0123456789abcdef'
let hex
while (num) {
hex = HEXS.charAt(num % 16) + hex
num = Math.floor(num / 16)
}
return hex
}
function hex2Int (hex = '') {
if (typeof hex !== 'string' || hex === '') {
return NaN
}
const hexs = [...hex.toLowerCase()]
let resInt = 0
for (let i = 0; i < hexs.length; i++) {
const hv = hexs[i]
let num = hv.charCodeAt() < 58 ? +hv : ((code - 97) + 10)
resInt = resInt * 16 + num
}
return resInt
}
如果要轉換八進制,實際上與十六進制很類似,只需根據八進制的數值範圍進行部分改動即可。八進制一般使用非常少,不單獨列出。
下面將重點介紹二進制轉換的相關知識,包括小數的二進制表示與轉換。
十進制和二進制轉換
在十進制與二進制的轉換中,我們將考慮小數,理解小數是如何在這兩者之間進行轉換。
先選定一個數字,比如:11.125
,我們看下該數字在二進制里的表示:
(11.125).toString(2) // 1011.001
可以看到,11.125
的二進制表示為:1011.001
。下面將以這個數字為例進行轉換操作。
十進制數字轉換成二進制
首先需要了解的是,二進制小數表示方法是如何得來的:
-
整數 部分,用二進制表示可以如此計算,數字 11:
11 / 2 ———— 1
5 / 2 ———— 1
2 / 2 ———— 0
1 / 2 ———— 1
整數部分的規則,得到的結果是從下往上
,倒着排1011
就是二進制的 11。 -
小數 用二進制表示可以如此計算,小數
0.125
:
例如十進制的 0.125
0.125 × 2 = 0.25 ———— 0
0.25 × 2 = 0.5 ———— 0
0.5 × 2 = 1 ———— 1
只有等於1時才結束,如果結果不等於1將會一直循環下去。
小數部分的規則,得到的結果是從上往下
,順着排0.001
就是二進制的0.125
。整數 + 小數,所以
11.125
的二進制表示方式:1011.001
。
根據以上整數和小數分開計算的規則,就可以得出十進制轉二進制的函數,如下:function c10to2 (num) { // 整數 const numInteger = Math.floor(num) // 小數 const numDecimal = num - numInteger let integers = [] if (numInteger === 0) { integers = ['0'] } else { let integerVal = numInteger while(integerVal !== 1) { integers.push(integerVal % 2 === 0 ? '0' : '1') integerVal = Math.floor(integerVal / 2) } integers.push('1') } const resInteger = integers.reverse().join('') let decimals = [] if (numDecimal) { let decimalVal = numDecimal // 最多取49位的長度 let count = 49 while (decimalVal !== 1 && count > 0) { decimalVal = decimalVal * 2 if (decimalVal >= 1) { decimals.push('1') if (decimalVal > 1) { decimalVal = decimalVal - 1 } } else { decimals.push('0') } count-- } } const resDecimal = decimals.join('') return resInteger + (resDecimal ? ('.' + resDecimal) : '') }
小數在轉換成二進制時,會存在無限循環的問題,上面的代碼里截取了前49個值。
所以,這裡就會引出了一個問題,就是常見的一個數字精度問題:0.1 + 0.2 != 0.3
。
0.1+ 0.2 != 0.3
直接看一下 0.1
轉二進制:
0.1 × 2 = 0.2
0.2 × 2 = 0.4
0.4 × 2 = 0.8
0.8 × 2 = 1.6
0.6 × 2 = 1.2
0.2 × 2 = 0.4 // 循環開始
0.4 × 2 = 0.8
0.8 × 2 = 1.6
0.6 × 2 = 1.2
…
…
無限循環
0.2
轉二進制:
0.2 × 2 = 0.4
0.4 × 2 = 0.8
0.8 × 2 = 1.6
0.6 × 2 = 1.2
0.2 × 2 = 0.4 // 循環開始
0.4 × 2 = 0.8
0.8 × 2 = 1.6
0.6 × 2 = 1.2
…
…
無限循環
因為無法得到1,可以發現有限十進制小數, 0.1
轉換成了無限二進制小數 0.00011001100...
,0.2
轉成了 0.001100110011...
。
由於無限循環,必然會導致精度丟失,正好 0.1 + 0.2
計算得到的數字在丟失精度後的最後一位不為0,所以導致結果為:0.30000000000000004
。
如果截取精度後最後一位為0,那自然就不存在結果不相等的情況,如 0.1 + 0.6 === 0.7
,事實上,0.1和0.6轉二進制後都會丟失精度,但截取到的數值都是0,所以相等。
同樣不相等的還設有 0.1 + 0.7 !== 0.8
等等。
所以是計算時轉二進制的精度丟失,才導致的 0.1 + 0.2 !== 0.3
。
在 JavaScript 中所有數值都以 IEEE-754 標準的 64 bit 雙精度浮點數進行存儲的。
IEEE 754 標準的 64 位雙精度浮點數的小數部分最多支持53位二進制位。
因浮點數小數位的限制而需要先截斷二進制數字,再轉換為十進制,所以在進行算術計算時會產生誤差。
這裡能看到,如果十進制小數要被轉化為有限二進制小數,那麼它計算後的小數第一位數必然要是 5
結尾才行(因為只有 0.5 × 2
才能變為整數)。
二進制數字轉換成十進制
方法是:將二進制分成整數和小數兩部分,分別進行轉換,然後再組合成結果的十進制數值。
-
整數部分:這裡直接使用
parseInt
函數,parseInt('1011', 2) => 11
。 -
小數部分:如
1011.001
的小數位001
,使用下表的計算方式。
小數部分|0|0|1
–|–|–|–
基數的位數次冪|2-1|2-2|2^-3
每位與基數乘積|0 × (2^-1)|0 × (2-2)|1×(2-3)
每位乘積結果|0|0|0.125最後的結果是每位乘積結果相加:
0+0+0.125 = 0.125
。
整數與小數合起來,就得到了 1011.001
的十進制數字:11.125
。
根據規則,代碼實現如下所示:
function c2To10 (binaryStr = '') {
if (typeof binaryStr !== 'string' || binaryStr === '') {
return NaN
}
const [ binIntStr, binDecStr ] = binaryStr.split('.')
let binDecimal = 0
if (binDecStr) {
binDecimal = [...binDecStr].reduce((res, val, index) => {
res += Number(val) * (2 ** (-(index + 1)))
return res
}, 0)
}
return parseInt(binIntStr, 2) + binDecimal
}