JavaScript中的字节数组转十六进制字符串转换

47

我有一个字节数组,形式为[4,-101,122,-41,-30,23,-28,3,..],我想将它转换成形式6d69f597b217fa333246c2c8。 我正在使用以下函数:

function toHexString(bytes) {
  return bytes.map(function(byte) {
    return (byte & 0xFF).toString(16)
  }).join('')
}

我得到的是同一形式的字符串,但我怀疑它不是有效的转换,因为十六进制字符串比预期的要短一些。我认为转换应该得到 "0a10a6dc"。 请告诉我我是否错误,或者这是正确的转换,但也许我没有使用正确的字节数组。

字节数组4,-127,45,126,58,-104,41,-27,-43,27,-35,100,-50,-77,93,-16,96,105,-101,-63,48,-105,49,-67,110,111,26,84,67,-89,-7,-50,10,-12,56,47,-49,-42,-11,-8,-96,-117,-78,97,-105,9,-62,-44,-97,-73,113,96,23,112,-14,-62,103,-104,90,-14,117,78,31,-116,-7

相应的转换:4812d7e3a9829e5d51bdd64ceb35df060699bc1309731bd6e6f1a5443a7f9ceaf4382fcfd6f5f8a08bb261979c2d49fb771601770f2c267985af2754e1f8cf9


抱歉,我已更新了代码。在发布之前我更改了变量,但现在我正在使用原始代码。 - Actung
10个回答

83

你在十六进制转换中缺少填充。你需要使用

function toHexString(byteArray) {
  return Array.from(byteArray, function(byte) {
    return ('0' + (byte & 0xFF).toString(16)).slice(-2);
  }).join('')
}

使每个字节转换为正好两个十六进制数字。您期望的输出是04812d7e3a9829e5d51bdd64ceb35df060699bc1309731bd6e6f1a5443a7f9ce0af4382fcfd6f5f8a08bb2619709c2d49fb771601770f2c267985af2754e1f8cf9


@DavidCallanan grantpatternson 在评论旧版本的答案时指出,我使用了 byteArray.map 而不是 Array.from - Bergi
同样的事情,但以更紧凑的形式:const toHexString = byteArray => Array.from(byteArray, byte => ('0' + (byte & 0xFF).toString(16)).slice(-2)).join('') - oittaa
1
& 0xFF 是什么意思? - Lance
2
@Lance 它将 byte 强制转换为介于 0 和 255 之间的整数。如果数组元素已经是无符号八位字节(“bytes”),则这是不必要的,但例如 OP 有带符号整数。 - Bergi

12

如果输入的类型为Uint8Array之类的类型,则使用map()将无法工作: map()的结果也是Uint8Array,无法保存字符串转换的结果。

function toHexString(byteArray) {
  var s = '0x';
  byteArray.forEach(function(byte) {
    s += ('0' + (byte & 0xFF).toString(16)).slice(-2);
  });
  return s;
}

警告:出现“致命错误:在堆限制附近的无效标记压缩,分配失败 - JavaScript堆内存不足”。 - Silvio Guedes

7
使用Array.reduce()的更简洁和高效(见https://jsperf.com/byte-array-to-hex-string)的替代方法:
function toHexString(byteArray) {
  return byteArray.reduce((output, elem) => 
    (output + ('0' + elem.toString(16)).slice(-2)),
    '');
}

(同样,没有"&0xFF",因为我认为如果传入的数组包含大于255的值,则输出应该混乱不堪,以便用户更容易看到他们的输入有误。)
(提示:"&"是HTML中的转义字符,翻译为"&"即可。)

1
请注意,如果byteArray包含从-128到127而不是0到255的有符号字节,则可能需要&0xFF - looper
我很惊讶这个更高效,因为它看起来在字节数组长度上的运行时间是二次的。你只是在小数组上尝试过吗(顺便说一下,jsperf链接已经失效了)?还是Javascript确实很聪明地在每次reduce迭代中重复使用output位置? - Alex Coventry

7

所有之前的解决方案都是可行的,但是它们都需要创建许多字符串,并对创建的字符串进行连接和切片。我开始思考是否有更好的方法来处理它,现在有了类型化数组。最初我使用了node完成了这个过程,然后注释掉了使用Buffer的行,并将它们改为TypedArrays,以便在浏览器中也能正常工作。

这样做会增加一些代码量,但是它的速度显著提高,在我编写的快速jsperf(不再工作)中,与被接受的答案中的字符串操作版本相比,下面的代码执行速度达到了317000次/秒,而字符串操作版本执行速度只有37000次/秒。创建字符串对象时存在很多隐藏的开销。

function toHexString (byteArray) {
  //const chars = new Buffer(byteArray.length * 2);
  const chars = new Uint8Array(byteArray.length * 2);
  const alpha = 'a'.charCodeAt(0) - 10;
  const digit = '0'.charCodeAt(0);

  let p = 0;
  for (let i = 0; i < byteArray.length; i++) {
      let nibble = byteArray[i] >>> 4;
      chars[p++] = nibble > 9 ? nibble + alpha : nibble + digit;
      nibble = byteArray[i] & 0xF;
      chars[p++] = nibble > 9 ? nibble + alpha : nibble + digit;    
  }

  //return chars.toString('utf8');
  return String.fromCharCode.apply(null, chars);
}

现代浏览器几乎都支持类型数组(即使是IE 10和11)。 - bmacnaughton
如果输入的数组是有符号的,则需要将此行let nibble = byteArray[i] >>> 4更改为let nibble = byteArray[i] >>> 4&0xF; - bmacnaughton

7

因为这是“js byte to hex”在谷歌上的第一个搜索结果,而我也需要一些时间来理解Bergi的函数,所以我重写了该函数并添加了一些注释,使其更易于理解:

function byteToHex(byte) {
  // convert the possibly signed byte (-128 to 127) to an unsigned byte (0 to 255).
  // if you know, that you only deal with unsigned bytes (Uint8Array), you can omit this line
  const unsignedByte = byte & 0xff;

  // If the number can be represented with only 4 bits (0-15), 
  // the hexadecimal representation of this number is only one char (0-9, a-f). 
  if (unsignedByte < 16) {
    return '0' + unsignedByte.toString(16);
  } else {
    return unsignedByte.toString(16);
  }
}

// bytes is an typed array (Int8Array or Uint8Array)
function toHexString(bytes) {
  // Since the .map() method is not available for typed arrays, 
  // we will convert the typed array to an array using Array.from().
  return Array.from(bytes)
    .map(byte => byteToHex(byte))
    .join('');
}

原帖忘记在只显示4位的数字前添加前导0


1

在我看来,使用Typescript可以提供更简单的解决方案:

const convertHashToHex = (value: TypedArray | number[]) : string => {
  return value.map(v => v.toString(16).padStart(2, '0')).join('');
} 

JS版本:

const convertHashToHex = (value) => {
  return value.map(v => v.toString(16).padStart(2, '0')).join('');
} 

这个的反过来呢?请转换 convertHexToHash :) - Mecanik
这与原问题无关,但如果您打开它,我很乐意回答。 - Roberto

0

这是一个适用于所有浏览器的ArrayBuffer解决方案:

    function buf2hex(buffer) {
        var u = new Uint8Array(buffer),
            a = new Array(u.length),
            i = u.length;
        while (i--) // map to hex
            a[i] = (u[i] < 16 ? '0' : '') + u[i].toString(16);
        u = null; // free memory
        return a.join('');
    };

0
你需要在十六进制转换中添加适当数量的前导零。

0

在将字节数组转换为十六进制数组时,我们必须考虑它们是否可以是有符号数。如果是这样,我们需要先将它们转换为十进制数。有符号数转换为十进制数。然后,我们可以使用.toString(16)方法将其转换为十六进制。

const hexArr = byteArr.map((byte) => {
    if (byte < 0) {
      byte = -((byte ^ 0xff) + 1); //converting 2s complement to a decimal number
    }
    //add padding at the start to ensure it's always 2 characters long otherwise '01' will be '1'
    return byte.toString(16).padStart(2, '0'); 
});

-5
如果在Nodejs上运行,只需使用Buffer.toString('base64')。
crypto.randomBytes(byteLength).toString('base64')

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接