进制介绍

JavaScript中提供了四种表示进制的方法:二进制、八进制、十进制、十六进制。
不同的数值类型相应地有不同的数值字面量格式,主要是使用不同的前缀来区分:

  • 二进制(Binary):取值数字 0 和 1 ;前缀 0b 或 0B。
  • 八进制(Octal):取值数字 0-7 ;前缀 0o 或 0O。
  • 十进制(Decimal):取值数字 0-9;不用前缀。
  • 十六进制(Hexadecimal):取值数字 0-9 和 a-f ;前缀 0x 或 0X。

二进制数字的表现形式,是ES6中新增的功能,用于避免在以前的版本中使用parseInt函数转换二进制字符串到十进制数字时的不便。
例如,要表示二进制数1011,可以这样写:

let binaryNumber = 0b1011;
console.log(binaryNumber); // 11

这样,就可以直接在代码中书写二进制数了,而不必使用以前常用的parseInt方法:

let binaryNumber = parseInt("1011", 2);
console.log(binaryNumber); // 11

使用0b前缀更加简洁和直观。

对于八进制字面量,在ES6之前前缀使用0表示八进制。如果前缀0后面是相应的八进制数字(0~7),该数值被视为八进制;但如果前缀0后面跟随的数字中有 8或者9,则视为十进制。
需要注意的是,ES5中新增了严格模式,在严格模式下前缀为0的八进制字面量是无效的,并且还会导致Javascript引擎抛出语法错误。在严格模式下,我们需要使用前缀为0O的八进制字面量。

默认情况下,二进制、八进制、十六进制字面量数值,都会自动转为十进制进行运算。

0b110111   // 55
0o67       // 55
0x37       // 55

0b110111 + 0o67        // 110
(0b110111).toString()  // 55
(0b110111).valueOf()   //55

原生进制转换

JavaScript 提供了原生函数,进行十进制与其他各进制之间的相互转换。
其中,从其他进制转换成十进制,有三种方式:parseInt(),Number(),+(一元运算符)。这三种方式都只能转换整数。
从十进制转换成其他进制,可以使用 Number.prototype.toString()。支持小数。

parseInt(str, radix)

parseInt()函数从字符串第一个非空格字符开始转换,如果第一个字符不是数值字符、加号或减号,则立即返回NaN。如果第一个字符是数值字符、加号或减号,则继续依次检测每个字符,直到字符串末尾或碰到非数值字符。比如“123blue”会被转换为123,因为“blue”会被忽略。类似的“22.5”会被转换为22,因为小数点不是有效的整数字符。

parseInt()接收两个参数。第一个参数是需要解析的字符串。
第二个参数是进制转换基数,表示转换时按什么进制来理解这个字符串,范围2~36,默认值10,表示转十进制。如果是非数字,则自动转数字,如无法转成数字则忽略该参数(忽略后使用默认的10进制)。

如果不传入第二参数,则 parseInt 会默认使用十进制来解析字符串;但是,如果字符串以0x开头,会被认为是十六进制数。而其他进制的字符串,0o67(八进制),0b110111(二进制) 不会以该进制基数自动转换,而是得到0。因此,在使用parseInt进行进制转换时,为了保证运行结果的正确性和稳定性,建议始终传入第二个参数。传入第一个参数后,第二个参数不再需要使用前缀。

// 二进制转十进制
parseInt("0b110111")  // 0
parseInt("110111", 2) // 55

// 八进制转十进制
parseInt("0o67", 8)   // 0
parseInt("67", 8)     // 55

// 十进制
parseInt("55")        // 55
parseInt("55", 10)    // 55

// 十六进制转十进制
parseInt("0x37")      // 55
parseInt("0x37", 16)  // 55
parseInt("37", 16)    // 55

Number(str)

Number()可以识别出不同的进制格式,二进制、八进制、十六进制都可以识别。字符串中如果存在无效的进制字符时,返回 NaN。

Number("0b110111") // 55
Number("0o67")     // 55
Number("55")       // 55
Number("0x37")     // 55
Number("0x37g")    // NaN, 包含无效字符g

+(一元运算符)

与 Number() 一样,可以把字符串转为数字,支持二进制、八进制、十六进制的字符串,默认转成十进制数字。

Number.prototype.toString(radix)

它支持传入一个进制转换基数,范围2~36,默认值为 10,用于将数字转换成对应进制的字符串,它支持转换小数。

15..toString(2)      // 1111
585..toString(8)     // 1111
4369..toString(16)   // 1111
(11.25).toString(2)  // 1011.01

自定义转换

除了js提供的这些原生函数以外,也可以自己实现进制数字之间的转换函数。根据相应的规则,就可以实现各种进制之间转换的一些方法。

十进制与十六进制转换

十进制转十六进制:
十进制转十六进制,使用除基取余,倒叙排列法。

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;
}

如果要转换八进制,转换方式与十六进制很类似,只需根据八进制的数值范围进行部分改动即可。

十进制和二进制转换

在十进制与二进制的转换中,我们将考虑小数。
十进制数字转换成二进制
将数值分成整数和小数两个部分,整数部分使用除基取余,倒叙排列法,小数部分使用乘基取整,正序排列法。

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;
    // 小数在转换成二进制时,会存在无限循环的问题,截取前53个值
    let count = 53;
    // 规格化时会左移,从第一位是1的地方开始记录
    let startRecord = false;
    while (decimalVal !== 1 && count > 0) {
      decimalVal = decimalVal * 2;
      if (decimalVal >= 1) {
        startRecord = true;
        decimals.push('1');
        if (decimalVal > 1) {
          decimalVal = decimalVal - 1;
        }
      } else {
        decimals.push('0')
      }
      if (startRecord) {
           count--;     
      }     
    }
  }
  const resDecimal = decimals.join('');

  return resInteger + (resDecimal ? ('.' + resDecimal) : '');
}

注意,小数在转换成二进制时,会存在无限循环的问题,上面的代码里截取了前53个值。
1.jpg

二进制数字转换成十进制
方法是:将二进制分成整数和小数两部分,分别进行转换,然后再组合成结果的十进制数值。
整数部分:这里直接使用 parseInt 函数,parseInt('1011', 2) => 11。
小数部分:如 1011.001 的小数位 001,使用按位权展开的多项式,0(2^-1) + 0(2^-2) + 1*(2^-3) = 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;
}

结语

篇幅有限,自定义转换部分没有将所有的转换规则罗列出来,可以参考我之前讲述进制转换规则的文章,举一反三自行实现。JS提供的四种进制为我们表示各种数值形式提供了很大的便利,除了十进制是Javascript默认的数字进制以外,其他三种进制方式平时使用较少,主要在处理Blob数据、字节编码或者位运算、转义字符等等时候才会碰到。各进制间通过一定的规则可以相互转换,这就让我们不用担心数据类型不一致的问题。