JavaScript中的多種進位制與進位制轉換

jimojianghu發表於2021-11-30

進位制介紹

JavaScript 中提供的進製表示方法有四種:十進位制、二進位制、十六進位制、八進位制。
對於數值字面量,主要使用不同的字首來區分:

  • 十進位制(Decimal):
    取值數字 0-9;不用字首。
  • 二進位制(Binary):
    取值數字 01 ;字首 0b0B
  • 十六進位制(Hexadecimal):
    取值數字 0-9a-f ;字首 0x0X
  • 八進位制(Octal):
    取值數字 0-7 ;字首 0o0O (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
記住,需要使用進位制字首,0b0o0x

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-9a-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 才能變為整數)。

二進位制數字轉換成十進位制

方法是:將二進位制分成整數和小數兩部分,分別進行轉換,然後再組合成結果的十進位制數值。

  1. 整數部分:這裡直接使用 parseInt 函式,parseInt('1011', 2) => 11

  2. 小數部分:如 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
}

相關文章