如何将字符串转换为字节数组

130

我该如何使用 JavaScript 将字符串转换为 bytearray。输出结果应与下面的 C# 代码等价。

UnicodeEncoding encoding = new UnicodeEncoding();
byte[] bytes = encoding.GetBytes(AnyString);

由于UnicodeEncoding默认使用UTF-16与小端序。

编辑:我有一个要求,需要将客户端生成的字节数组与使用上述C#代码在服务器端生成的字节数组进行匹配。


3
JavaScript并不是以与BLOBs(二进制数据)易用而闻名的语言 - 为什么不直接将字符串作为JSON发送? - Marc Gravell
也许你可以在这里看一下:https://dev59.com/b3A75IYBdhLWcg3wo6he - V4Vendetta
2
JavaScript字符串是UTF-16编码的,你已经知道了吗? - Kevin
2
首先,为什么你需要将这个转换成JavaScript? - BreakHead
22
字符串没有被编码。是的,在内部它们被表示为字节并且有一种编码方式,但在脚本级别上这基本上没有意义。字符串是逻辑字符集合。要对一个字符进行编码,你必须明确地选择一个编码方案,然后使用它将每个字符代码转换为一个或多个字节序列。这个问题下面的答案是垃圾,因为它们调用charCodeAt并将其值插入到名为"bytes"的数组中。你好! charCodeAt返回的值可以大于255,所以不是一个字节! - Triynko
显示剩余4条评论
12个回答

63

2018年更新 - 2018年最简单的方法应该是使用TextEncoder

let utf8Encode = new TextEncoder();
utf8Encode.encode("abc");
// Uint8Array [ 97, 98, 99 ]

注意事项 - 返回的元素是一个Uint8Array,并且不是所有浏览器都支持它


这很奇怪。我不认为使用不同的变量名称,如utf8Decode和utf8Encode会起作用。 - Unihedron
你可以使用TextDecoder来解码:new TextDecoder().decode(new TextEncoder().encode(str)) == str - Fons
关于“不是所有浏览器”的澄清。该表中唯一一个现在不支持它的现代浏览器是Opera Mini。 - General Grievance

40

如果你正在寻找在node.js中运行的方案,你可以使用这个:

var myBuffer = [];
var str = 'Stack Overflow';
var buffer = new Buffer(str, 'utf16le');
for (var i = 0; i < buffer.length; i++) {
    myBuffer.push(buffer[i]);
}

console.log(myBuffer);

4
这是针对Node.js的,但我认为问题正在寻找在浏览器中运行的解决方案。尽管如此,它确实可以正常工作,与此问题的大多数其他答案不同,因此+1。 - Daniel Cassidy
这段代码可以运行,但是有更简单的代码:function convertString(myString) { var myBuffer = new Buffer(myString, 'utf16le'); console.log(myBuffer); return myBuffer; } - Philip Rutovitz
1
由于新的Buffer已被弃用,因此应该使用from方法:var buffer = Buffer.from(str, 'utf16le'); - Geoffrey Bourne
2
截至2021年11月5日,new Buffer失败,因为Chrome浏览器中未定义Buffer - PatS
@PatS 这不是用于浏览器的。 - Unmitigated

31

在 C# 中运行此代码

UnicodeEncoding encoding = new UnicodeEncoding();
byte[] bytes = encoding.GetBytes("Hello");

将创建一个数组

72,0,101,0,108,0,108,0,111,0

字节数组

如果字符的代码大于255,它将如此显示

字节数组

如果你想在JavaScript中获得非常相似的行为,可以这样做(v2 是一个更健壮的解决方案,而原始版本只能用于 0x00 ~ 0xff)

var str = "Hello竜";
var bytes = []; // char codes
var bytesv2 = []; // char codes

for (var i = 0; i < str.length; ++i) {
  var code = str.charCodeAt(i);
  
  bytes = bytes.concat([code]);
  
  bytesv2 = bytesv2.concat([code & 0xff, code / 256 >>> 0]);
}

// 72, 101, 108, 108, 111, 31452
console.log('bytes', bytes.join(', '));

// 72, 0, 101, 0, 108, 0, 108, 0, 111, 0, 220, 122
console.log('bytesv2', bytesv2.join(', '));


2
@shas 我之前只在 Firefox 4 上测试过,更新后的版本已经在 Firefox 4、Chrome 13 和 IE9 上进行了测试。 - BrunoLM
41
请注意,如果字符串包含Unicode字符,charCodeAt(i)将大于255,这可能不是你想要的。 - broofa
25
是的,这是不正确的。 charCodeAt 不会返回一个字节。将大于255的值推入名为“bytes”的数组毫无意义,非常具有误导性。该函数根本没有执行编码操作,它只是将字符代码插入到一个数组中。 - Triynko
2
我不明白为什么这个答案被标记为正确,因为它没有编码任何东西。 - A.B.
1
这完全是错误的。一个字符不等于一个字节。对于任何超出 U+0000 - U+00FF 范围的字符,这都会失败。 - Daniel Cassidy
显示剩余4条评论

16

我想C#和Java生成的字节数组是相等的。如果您有非ASCII字符,仅添加额外的0是不够的。我的示例包含一些特殊字符:

var str = "Hell ö € Ω ";
var bytes = [];
var charCode;

for (var i = 0; i < str.length; ++i)
{
    charCode = str.charCodeAt(i);
    bytes.push((charCode & 0xFF00) >> 8);
    bytes.push(charCode & 0xFF);
}

alert(bytes.join(' '));
// 0 72 0 101 0 108 0 108 0 32 0 246 0 32 32 172 0 32 3 169 0 32 216 52 221 30

我不知道C#是否会添加字节顺序标记(Byte Order Marks),但如果使用UTF-16,Java的String.getBytes方法会添加以下字节:254 255。

String s = "Hell ö € Ω ";
// now add a character outside the BMP (Basic Multilingual Plane)
// we take the violin-symbol (U+1D11E) MUSICAL SYMBOL G CLEF
s += new String(Character.toChars(0x1D11E));
// surrogate codepoints are: d834, dd1e, so one could also write "\ud834\udd1e"

byte[] bytes = s.getBytes("UTF-16");
for (byte aByte : bytes) {
    System.out.print((0xFF & aByte) + " ");
}
// 254 255 0 72 0 101 0 108 0 108 0 32 0 246 0 32 32 172 0 32 3 169 0 32 216 52 221 30

编辑:

增加了一个特殊字符(U+1D11E)MUSICAL SYMBOL G CLEF(不在BPM之内,因此占用的是UTF-16中的4个字节而不仅仅是2个字节。

当前的JavaScript版本在内部使用“UCS-2”,因此该符号占据了2个普通字符的空间。

我不确定,但当使用charCodeAt时,似乎我们得到的恰好是UTF-16中也使用的代理码位,因此非BMP字符会被正确处理。

这个问题绝对不是简单的。它可能取决于所使用的JavaScript版本和引擎。因此,如果您想要可靠的解决方案,应该查看以下链接:


1
仍然不是一个完整的答案。UTF16是一种可变长度编码,使用16位块来表示字符。单个字符将被编码为2个字节或4个字节,具体取决于字符代码值的大小。由于此函数最多只写入2个字节,因此它无法处理所有Unicode字符代码点,并且不是UTF16编码的完整实现,远远不够。 - Triynko
@Triynko,我编辑并测试过了,您仍然认为这不是完整的答案吗?如果是,您有答案吗? - hgoebl
2
@Triynko 你说的一半对,但实际上这个答案是正确的。JavaScript字符串实际上不是Unicode代码点序列,它们是UTF-16代码单元序列。尽管名称如此,charCodeAt返回的是一个UTF-16代码单元,在0-65535范围内。超出2字节范围的字符被表示为代理对,就像在UTF-16中一样。(顺便说一下,这也适用于其他几种语言中的字符串,包括Java和C#。) - Daniel Cassidy
顺便说一下,(charCode & 0xFF00) >> 8 是多余的。在移位之前,您不需要对其进行掩码处理。 - Patrick Roberts

15

UTF-16字节数组

JavaScript将字符串编码为UTF-16,就像C#的UnicodeEncoding一样,因此创建字节数组相对简单。

JavaScript的charCodeAt()返回一个16位的代码单元(也称为介于0和65535之间的2字节整数)。您可以使用以下方法将其分割成不同的字节:

function strToUtf16Bytes(str) {
  const bytes = [];
  for (ii = 0; ii < str.length; ii++) {
    const code = str.charCodeAt(ii); // x00-xFFFF
    bytes.push(code & 255, code >> 8); // low, high
  }
  return bytes;
}
例如:
strToUtf16Bytes(''); 
// [ 60, 216, 53, 223 ]

这在C#和JavaScript之间有效,因为它们都支持UTF-16。然而,如果您想从JS获取UTF-8字节数组,则必须转换字节。

UTF-8字节数组

解决方案感觉有些复杂,但我在生产中使用了下面的代码并取得了巨大成功(原始来源)。

此外,对于感兴趣的读者,我发布了我的Unicode辅助工具,可帮助我处理其他语言(如PHP)报告的字符串长度问题。

/**
 * Convert a string to a unicode byte array
 * @param {string} str
 * @return {Array} of bytes
 */
export function strToUtf8Bytes(str) {
  const utf8 = [];
  for (let ii = 0; ii < str.length; ii++) {
    let charCode = str.charCodeAt(ii);
    if (charCode < 0x80) utf8.push(charCode);
    else if (charCode < 0x800) {
      utf8.push(0xc0 | (charCode >> 6), 0x80 | (charCode & 0x3f));
    } else if (charCode < 0xd800 || charCode >= 0xe000) {
      utf8.push(0xe0 | (charCode >> 12), 0x80 | ((charCode >> 6) & 0x3f), 0x80 | (charCode & 0x3f));
    } else {
      ii++;
      // Surrogate pair:
      // UTF-16 encodes 0x10000-0x10FFFF by subtracting 0x10000 and
      // splitting the 20 bits of 0x0-0xFFFFF into two halves
      charCode = 0x10000 + (((charCode & 0x3ff) << 10) | (str.charCodeAt(ii) & 0x3ff));
      utf8.push(
        0xf0 | (charCode >> 18),
        0x80 | ((charCode >> 12) & 0x3f),
        0x80 | ((charCode >> 6) & 0x3f),
        0x80 | (charCode & 0x3f),
      );
    }
  }
  return utf8;
}

这的反过来是什么? - simbo1905
我会将反函数描述为“将UTF-8字节数组转换为本地的UTF-16字符串”。我从未生成过反函数。在我的环境中,我通过将API输出更改为字符范围而不是字节范围来删除了此代码,然后我使用runes来解析这些范围。 - jchook
我建议这个回答应该成为这个问题的被采纳答案。 - LeaveTheCapital

11

受@hgoebl答案的启发。他的代码适用于UTF-16,我需要一些针对US-ASCII的内容。因此,这里有一个更完整的答案,涵盖了US-ASCII,UTF-16和UTF-32。

/**@returns {Array} bytes of US-ASCII*/
function stringToAsciiByteArray(str)
{
    var bytes = [];
   for (var i = 0; i < str.length; ++i)
   {
       var charCode = str.charCodeAt(i);
      if (charCode > 0xFF)  // char > 1 byte since charCodeAt returns the UTF-16 value
      {
          throw new Error('Character ' + String.fromCharCode(charCode) + ' can\'t be represented by a US-ASCII byte.');
      }
       bytes.push(charCode);
   }
    return bytes;
}
/**@returns {Array} bytes of UTF-16 Big Endian without BOM*/
function stringToUtf16ByteArray(str)
{
    var bytes = [];
    //currently the function returns without BOM. Uncomment the next line to change that.
    //bytes.push(254, 255);  //Big Endian Byte Order Marks
   for (var i = 0; i < str.length; ++i)
   {
       var charCode = str.charCodeAt(i);
       //char > 2 bytes is impossible since charCodeAt can only return 2 bytes
       bytes.push((charCode & 0xFF00) >>> 8);  //high byte (might be 0)
       bytes.push(charCode & 0xFF);  //low byte
   }
    return bytes;
}
/**@returns {Array} bytes of UTF-32 Big Endian without BOM*/
function stringToUtf32ByteArray(str)
{
    var bytes = [];
    //currently the function returns without BOM. Uncomment the next line to change that.
    //bytes.push(0, 0, 254, 255);  //Big Endian Byte Order Marks
   for (var i = 0; i < str.length; i+=2)
   {
       var charPoint = str.codePointAt(i);
       //char > 4 bytes is impossible since codePointAt can only return 4 bytes
       bytes.push((charPoint & 0xFF000000) >>> 24);
       bytes.push((charPoint & 0xFF0000) >>> 16);
       bytes.push((charPoint & 0xFF00) >>> 8);
       bytes.push(charPoint & 0xFF);
   }
    return bytes;
}

UTF-8是可变长度的编码,因为我需要自己编写这种编码,所以它没有被包含在内。UTF-8和UTF-16都是可变长度的编码。UTF-8、UTF-16和UTF-32都有一个最小位数,就像它们的名称一样。如果一个UTF-32字符的代码点是65,那么意味着有3个前导0。但是对于UTF-16来说,相同的代码只有1个前导0。另一方面,US-ASCII是固定的8位宽度,这意味着它可以直接转换为字节。

String.prototype.charCodeAt返回最多2个字节,并与UTF-16完全匹配。但是对于UTF-32,需要使用String.prototype.codePointAt,它是ECMAScript 6(Harmony)提案的一部分。因为charCodeAt返回2个字节,这比US-ASCII能表示的字符更多,所以函数stringToAsciiByteArray在这种情况下会抛出异常,而不是将字符分成两半并获取其中一个或两个字节。

请注意,这个答案不是简单的问题,因为字符编码不是简单的问题。你想要什么样的字节数组取决于你想让这些字节代表什么字符编码。

Javascript内部可以使用UTF-16或UCS-2,但是由于它有像使用UTF-16一样的方法,我不明白为什么任何浏览器会使用UCS-2。

同时查看: https://mathiasbynens.be/notes/javascript-encoding

是的,我知道这个问题已经4年了,但我需要这个答案。


Node的Buffer对于'02'的结果是[ 48, 0, 50, 0 ],而你的stringToUtf16ByteArray函数返回[ 0, 48, 0, 50 ]。哪一个是正确的? - Philipp Kyeck
@pkyeck,我上面的stringToUtf16ByteArray函数返回没有BOM的UTF-16 BE格式。你提供的来自node的示例是没有BOM的UTF-16 LE格式。我曾认为大端序比小端序更常见,但可能是错误的。 - SkySpiral7

2
由于我无法在答案中进行评论,因此我将在 Jin Izzraeel 的答案基础上进行补充。
var myBuffer = [];
var str = 'Stack Overflow';
var buffer = new Buffer(str, 'utf16le');
for (var i = 0; i < buffer.length; i++) {
    myBuffer.push(buffer[i]);
}

console.log(myBuffer);
通过这种方式,如果您想在浏览器中使用Node.js缓冲区,则可以使用它。https://github.com/feross/buffer因此,Tom Stickel的反对意见是无效的,答案确实是有效的答案。

1
String.prototype.encodeHex = function () {
    return this.split('').map(e => e.charCodeAt())
};

String.prototype.decodeHex = function () {    
    return this.map(e => String.fromCharCode(e)).join('')
};

4
请提供一些文本来解释为什么选择这种方法而不是其他答案,这将有助于理解代码。 - NightOwl888
这种方法比其他方法更简单,但效果相同,这就是我没有写任何东西的原因。 - Fabio Maciel
encodeHex将返回一个16位数字的数组,而不是字节。 - Pavlo

0

我知道这个问题已经快4年了,但是以下方法对我非常有效:

String.prototype.encodeHex = function () {
  var bytes = [];
  for (var i = 0; i < this.length; ++i) {
    bytes.push(this.charCodeAt(i));
  }
  return bytes;
};

Array.prototype.decodeHex = function () {    
  var str = [];
  var hex = this.toString().split(',');
  for (var i = 0; i < hex.length; i++) {
    str.push(String.fromCharCode(hex[i]));
  }
  return str.toString().replace(/,/g, "");
};

var str = "Hello World!";
var bytes = str.encodeHex();

alert('The Hexa Code is: '+bytes+' The original string is:  '+bytes.decodeHex());

或者,如果你只想使用字符串而不是数组,你可以使用:

String.prototype.encodeHex = function () {
  var bytes = [];
  for (var i = 0; i < this.length; ++i) {
    bytes.push(this.charCodeAt(i));
  }
  return bytes.toString();
};

String.prototype.decodeHex = function () {    
  var str = [];
  var hex = this.split(',');
  for (var i = 0; i < hex.length; i++) {
    str.push(String.fromCharCode(hex[i]));
  }
  return str.toString().replace(/,/g, "");
};

var str = "Hello World!";
var bytes = str.encodeHex();

alert('The Hexa Code is: '+bytes+' The original string is:  '+bytes.decodeHex());


2
这种方法似乎可行,但是极易引起误解。bytes 数组并不包含“字节”,而是包含 16 位数字,这些数字代表 UTF-16 编码单元中的字符串。这几乎符合问题的要求,但实际上只是偶然符合。 - Daniel Cassidy

0
我在现场想到的最好的解决方案(虽然很可能粗糙)是:
String.prototype.getBytes = function() {
    var bytes = [];
    for (var i = 0; i < this.length; i++) {
        var charCode = this.charCodeAt(i);
        var cLen = Math.ceil(Math.log(charCode)/Math.log(256));
        for (var j = 0; j < cLen; j++) {
            bytes.push((charCode << (j*8)) & 0xFF);
        }
    }
    return bytes;
}

尽管我注意到这个问题已经存在了一年多。

2
这个程序没有正确运行。变长字符逻辑不正确,UTF-16 中没有8位字符。虽然名字是 charCodeAt,但它返回的是一个16位的 UTF-16 代码单元,所以你不需要任何变长逻辑。你可以调用 charCodeAt,将结果分成两个8位字节,并将它们塞入输出数组中(从最低位字节开始,因为问题要求使用 UTF-16LE)。 - Daniel Cassidy

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