在字符串和ArrayBuffer之间进行转换

466

是否有一种通常被接受的技术,能够有效地将JavaScript字符串转换为ArrayBuffers,反之亦然?具体而言,我希望能够将一个ArrayBuffer的内容写入localStorage中,并随后读取。


1
我在这方面没有任何经验,但根据API文档(http://www.khronos.org/registry/typedarray/specs/latest/)的判断,如果您构建一个`Int8Array` ArrayBufferView,可能可以简单地使用括号表示法来复制字符string[i] = buffer[i],反之亦然。 - FK82
2
@FK82,这看起来是一个合理的方法(使用Uint16Array来处理JS的16位字符),但JavaScript字符串是不可变的,所以您不能直接赋值给一个字符位置。我仍然需要将Uint16Array中每个值的String.fromCharCode(x)复制到普通的Array中,然后在Array上调用.join() - kpozin
7
原文:@kpozin 发现大多数现代 JS 引擎已经优化了字符串拼接,以至于仅使用string += String.fromCharCode(buffer[i]);更加高效。没有内置方法在字符串和类型数组之间转换似乎很奇怪。他们本应该知道会出现这样的情况。翻译:发现大多数现代JavaScript引擎已经将字符串拼接优化到一个程度,在使用“string += String.fromCharCode(buffer[i])”时比其他方式更为便宜。没有内置的方法可以在字符串和类型数组之间进行转换,这似乎有点奇怪,因为他们应该知道这种情况会出现。 - Erin
1
arrayBuffer.toString() 对我来说运行良好。 - citizen conn
1
@citizen conn - 我不知道你使用的浏览器是什么,但在 Chrome 上,arrayBuffer.toString() 只会返回 "[object ArrayBuffer]"。并不是很有帮助。 - mrec
显示剩余4条评论
29个回答

319

2016年更新 - 经过五年的发展,现在有新的方法(请参见下面的支持)在规范中用于使用适当的编码在字符串和类型化数组之间进行转换。

TextEncoder

TextEncoder 表示:

TextEncoder 接口表示特定方法(即特定字符编码,如 utf-8iso-8859-2koi8cp1261gbk)的编码器。编码器将代码点流作为输入并发出字节流。

自上文以来的更改说明:(同上)

注意:Firefox、Chrome 和 Opera 曾经支持除 utf-8 以外的编码类型(例如 utf-16、iso-8859-2、koi8、cp1261 和 gbk)。从 Firefox 48[...]、Chrome 54[...] 和 Opera 41 开始,除了 utf-8 之外,不再提供其他编码类型,以匹配规范。

*) 更新的规范 (W3) 点此查看,whatwg 点此查看

创建 TextEncoder 的实例后,它将使用给定的编码参数对字符串进行编码:

if (!("TextEncoder" in window)) 
  alert("Sorry, this browser does not support TextEncoder...");

var enc = new TextEncoder(); // always utf-8
console.log(enc.encode("This is a string converted to a Uint8Array"));

然后你当然可以使用结果为Uint8Array.buffer参数将底层的ArrayBuffer转换为不同的视图(如果需要的话)。

只需确保字符串中的字符符合编码模式,例如,在示例中使用UTF-8范围之外的字符会将其编码为两个字节而不是一个字节。

对于一般用途,您应该使用UTF-16编码,例如localStorage

TextDecoder

同样,相反的过程使用 TextDecoder

TextDecoder接口表示特定方法的解码器,即特定的字符编码,如utf-8iso-8859-2koi8cp1261gbk等。解码器将字节流作为输入,并发出代码点流。

所有可用的解码类型可以在此处找到。

if (!("TextDecoder" in window))
  alert("Sorry, this browser does not support TextDecoder...");

var enc = new TextDecoder("utf-8");
var arr = new Uint8Array([84,104,105,115,32,105,115,32,97,32,85,105,110,116,
                          56,65,114,114,97,121,32,99,111,110,118,101,114,116,
                          101,100,32,116,111,32,97,32,115,116,114,105,110,103]);
console.log(enc.decode(arr));

MDN的StringView库

除了使用现有的TextEncoder/TextDecoder之外,还可以使用StringView库(采用lgpl-3.0许可证),其目标是:

  • 创建基于JavaScript ArrayBuffer接口的字符串的类C接口(即字符代码数组 - 在JavaScript中为ArrayBufferView)
  • 创建一个高度可扩展的库,任何人都可以通过向对象StringView.prototype添加方法来扩展它
  • 创建一组方法,针对这些类似于字符串的对象(从现在开始:stringViews),这些方法严格地处理数字数组而不是创建新的不可变JavaScript字符串
  • 使用JavaScript默认的UTF-16 DOMStrings以外的Unicode编码

这样可以提供更多的灵活性。 但是,它需要我们链接或嵌入此库,而现代浏览器中已内置了TextEncoder/TextDecoder

支持

截至2018年7月:

TextEncoder(实验性质,正在成为标准)。

 Chrome    | Edge      | Firefox   | IE        | Opera     | Safari
 ----------|-----------|-----------|-----------|-----------|-----------
     38    |     ?     |    19°    |     -     |     25    |     -

 Chrome/A  | Edge/mob  | Firefox/A | Opera/A   |Safari/iOS | Webview/A
 ----------|-----------|-----------|-----------|-----------|-----------
     38    |     ?     |    19°    |     ?     |     -     |     38

°) 18: Firefox 18 implemented an earlier and slightly different version
of the specification.

WEB WORKER SUPPORT:

Experimental, On Standard Track

 Chrome    | Edge      | Firefox   | IE        | Opera     | Safari
 ----------|-----------|-----------|-----------|-----------|-----------
     38    |     ?     |     20    |     -     |     25    |     -

 Chrome/A  | Edge/mob  | Firefox/A | Opera/A   |Safari/iOS | Webview/A
 ----------|-----------|-----------|-----------|-----------|-----------
     38    |     ?     |     20    |     ?     |     -     |     38

Data from MDN - `npm i -g mdncomp` by epistemex

3
IE 和 Edge 不支持 TextDecoder:https://caniuse.com/#search=TextDecoder - Andrei Damian-Fekete
1
根据微软的说法,它正在开发中:https://developer.microsoft.com/zh-cn/microsoft-edge/platform/status/encodingstandard/ - Maurice Müller
1
一行代码:var encoder = 'TextEncoder' in window ? new TextEncoder() : {encode: function(str){return Uint8Array.from(str, function(c){return c.codePointAt(0);});}}; 这样你就可以使用 var array = encoder.encode('hello'); - Yeti
5
使用TextEncoder的问题在于,如果您在字符串中有二进制数据(例如图片),则不应使用TextEncoder(显然)。代码点大于127的字符会产生两个字节。为什么我在字符串中有二进制数据?cy.fixture(NAME, 'binary')cypress)会生成一个字符串。 - x-yuri
1
你应该在 new TextDecoder("encoding") 外面加上 try/catch,因为你可能支持 TextDecoder 但不支持你想要使用的字符编码。 - PHP Guru
显示剩余5条评论

202

尽管Dennis和gengkev使用Blob/FileReader方法的解决方案可以工作,但我不建议采用这种方法。这是一个将异步方法应用于简单问题的方法,速度比直接解决方案慢得多。我在html5rocks上发布了一篇文章,介绍了一种更简单(并且更快)的解决方案:

http://updates.html5rocks.com/2012/06/How-to-convert-ArrayBuffer-to-and-from-String

而该解决方案如下:

function ab2str(buf) {
  return String.fromCharCode.apply(null, new Uint16Array(buf));
}

function str2ab(str) {
  var buf = new ArrayBuffer(str.length*2); // 2 bytes for each char
  var bufView = new Uint16Array(buf);
  for (var i=0, strLen=str.length; i<strLen; i++) {
    bufView[i] = str.charCodeAt(i);
  }
  return buf;
}

编辑:

编码API有助于解决字符串转换问题。请查看来自Html5Rocks.com的Jeff Posnik对上述原始文章的回应。

摘录:

无论您需要使用哪种标准编码方式,编码API都可以轻松地在原始字节和本机JavaScript字符串之间进行转换。

<pre id="results"></pre>

<script>
  if ('TextDecoder' in window) {
    // The local files to be fetched, mapped to the encoding that they're using.
    var filesToEncoding = {
      'utf8.bin': 'utf-8',
      'utf16le.bin': 'utf-16le',
      'macintosh.bin': 'macintosh'
    };

    Object.keys(filesToEncoding).forEach(function(file) {
      fetchAndDecode(file, filesToEncoding[file]);
    });
  } else {
    document.querySelector('#results').textContent = 'Your browser does not support the Encoding API.'
  }

  // Use XHR to fetch `file` and interpret its contents as being encoded with `encoding`.
  function fetchAndDecode(file, encoding) {
    var xhr = new XMLHttpRequest();
    xhr.open('GET', file);
    // Using 'arraybuffer' as the responseType ensures that the raw data is returned,
    // rather than letting XMLHttpRequest decode the data first.
    xhr.responseType = 'arraybuffer';
    xhr.onload = function() {
      if (this.status == 200) {
        // The decode() method takes a DataView as a parameter, which is a wrapper on top of the ArrayBuffer.
        var dataView = new DataView(this.response);
        // The TextDecoder interface is documented at http://encoding.spec.whatwg.org/#interface-textdecoder
        var decoder = new TextDecoder(encoding);
        var decodedString = decoder.decode(dataView);
        // Add the decoded file's text to the <pre> element on the page.
        document.querySelector('#results').textContent += decodedString + '\n';
      } else {
        console.error('Error while requesting', file, this);
      }
    };
    xhr.send();
  }
</script>

19
很遗憾,我的HTML5 Rocks上的评论还未被批准。因此在这里简短回答一下。我仍然认为这不是正确的方法,因为您会错过很多字符,特别是因为大多数页面今天都使用UTF-8编码。一方面,对于更多特殊字符(比如亚洲字符),charCodeAt函数会返回一个4字节的值,因此它们会被切断。另一方面,简单的英文字符会使ArrayBuffer增长两倍(您使用2个字节来表示每个1个字节的字符)。想象一下通过WebSocket发送英文文本,它将需要两倍的时间(在实时环境中不好)。 - Dennis
10
三个例子:(1) "这是一段很棒的文字!" 在UTF8下为20字节,在Unicode下为40字节。 (2) "ÄÖÜ" 在UTF8下为6字节,在Unicode下为6字节。 (3) "☐☑☒" 在UTF8下为9字节,在Unicode下为6字节。如果您想将字符串存储为UTF8文件(通过Blob和File Writer API),您不能使用这两种方法,因为ArrayBuffer将是Unicode而不是UTF8。 - Dennis
4
我遇到了一个错误:Uncaught RangeError: Maximum call stack size exceeded。这可能是什么问题? - Jacob
8
@Dennis - JS字符串使用的是UCS2编码,而不是UTF8(或者甚至不是UTF16)- 这意味着charCodeAt()始终会返回0到65535之间的值。任何需要4个字节的UTF-8代码点都将由代理对表示(请参见http://en.wikipedia.org/wiki/UTF-16#Code_points_U.2B10000_to_U.2B10FFFF)- 也就是两个独立的16位UCS2值。 - broofa
7
@jacob - 我认为错误是因为apply()方法中传递的数组长度有限制。例如,String.fromCharCode.apply(null, new Uint16Array(new ArrayBuffer(246300))).length这个例子在Chrome中可以运行,但如果你将数组长度改为246301,就会出现RangeError异常。 - broofa
显示剩余23条评论

113
你可以使用编码规范中的TextEncoderTextDecoder,这些功能由stringencoding库提供支持,将字符串转换为ArrayBuffers并相互转换:
var uint8array = new TextEncoder().encode(string);
var string = new TextDecoder(encoding).decode(uint8array);

2
顺便提一下,这在Firefox中是默认可用的:https://developer.mozilla.org/en-US/docs/Web/API/TextDecoder.decode - Joel
2
点赞新的API,它们比奇怪的变通方法好多了! - Tomáš Zato
3
这对于所有类型的字符都不适用。 - David
6
不需要,谢谢。 - Evan Hu
抱怨...如果我有一个现有的ArrayBuffer,想要写入一个字符串,我猜我必须取出Uint8Array并再次复制它吗? - shaunc
显示剩余2条评论

48

Blob速度比String.fromCharCode(null, array);慢得多。

但如果数组缓冲区太大,String.fromCharCode(null, array);会失败。我发现最好的解决方法是将它分割成不会耗尽堆栈但速度比一次处理一个字符更快的操作。

对于较大的数组缓冲区,最好的解决方案是:

function arrayBufferToString(buffer){

    var bufView = new Uint16Array(buffer);
    var length = bufView.length;
    var result = '';
    var addition = Math.pow(2,16)-1;

    for(var i = 0;i<length;i+=addition){

        if(i + addition > length){
            addition = length - i;
        }
        result += String.fromCharCode.apply(null, bufView.subarray(i,i+addition));
    }

    return result;

}

我发现这种方法比使用 blob 快大约 20 倍。它还适用于超过 100MB 的大字符串。


4
我们应该采用这个解决方案,因为它比已接受的方案多解决了一个使用情境。 - sam
1
我收到了:“未捕获的INVALID错误:json解码:这不是json!” - dicroce

31

如果你在字符串中有二进制数据(从nodejs+readFile(..., 'binary')cypress+cy.fixture(..., 'binary')等获取),那么你不能使用TextEncoder。它只支持utf8。具有值>= 128的字节会被转换成2个字节。

ES2015:

a = Uint8Array.from(s, x => x.charCodeAt(0))

Uint8Array(33) [2, 134, 140, 186, 82, 70, 108, 182, 233, 40, 143, 247, 29, 76, 245, 206, 29, 87, 48, 160, 78, 225, 242, 56, 236, 201, 80, 80, 152, 118, 92, 144, 48

这是一个长度为33的Uint8Array数组,其中包含33个8位无符号整数。每个整数都在0到255之间。
s = String.fromCharCode.apply(null, a)

"ºRFl¶é(÷LõÎW0 Náò8ìÉPPv\0"


谢谢您提供的最简单的解决方案! - Sergii Shymko
有没有办法使fromCharCode能够在TypeScript中使用?它会报错说a不是一个number[] - philk
这应该是被接受的答案! - Aminadav Glickshtein
这是我在使用pdf.js时唯一有效的答案。谢谢! - Francisco Gomes

25

根据gengkev的答案,我创建了两种方式的函数,因为BlobBuilder可以处理字符串和ArrayBuffer:

function string2ArrayBuffer(string, callback) {
    var bb = new BlobBuilder();
    bb.append(string);
    var f = new FileReader();
    f.onload = function(e) {
        callback(e.target.result);
    }
    f.readAsArrayBuffer(bb.getBlob());
}

并且

function arrayBuffer2String(buf, callback) {
    var bb = new BlobBuilder();
    bb.append(buf);
    var f = new FileReader();
    f.onload = function(e) {
        callback(e.target.result)
    }
    f.readAsText(bb.getBlob());
}

一个简单的测试:

string2ArrayBuffer("abc",
    function (buf) {
        var uInt8 = new Uint8Array(buf);
        console.log(uInt8); // Returns `Uint8Array { 0=97, 1=98, 2=99}`

        arrayBuffer2String(buf, 
            function (string) {
                console.log(string); // returns "abc"
            }
        )
    }
)

在 arrayBuffer2String() 函数中,你是不是想调用 callback(...) 而不是 console.log()?否则 callback 参数将会被未使用。 - Dan Phillimore
这看起来是正确的方法 - 谢谢genkev和Dennis。似乎有点傻,没有同步完成这个任务的方式,但你能做什么呢... - kpozin
我真的很希望这对我有用,但是从字符串转换为ArrayBuffer不可靠。我正在创建一个具有256个值的ArrayBuffer,并且可以将其转换为长度为256的字符串。但是,如果我尝试将其转换回ArrayBuffer-取决于我的初始ArrayBuffer的内容-我会得到376个元素。如果您想尝试重现我的问题,我将我的ArrayBuffer视为Uint8Array中的16x16网格,其中计算值为a[y * w + x] = (x + y) / 2 * 16;。我已经尝试了许多不同的MIME类型使用getBlob("x"),但没有运气。 - Matt Cruikshank
18
BlobBuilder在较新版本的浏览器中已被弃用。将new BlobBuilder(); bb.append(buf);替换为new Blob([buf]),在第二个函数中将ArrayBuffer转换为UintArray,方法是使用new UintArray(buf)(或者适用于底层数据类型的其他方法),然后消除getBlob()调用。最后,为了清晰明了,将bb重命名为blob,因为它不再是BlobBuilder。 - sowbug
@sowbug,你有参考资料吗? - Dennis
显示剩余3条评论

21

只是

const buffer = thisReturnsBuffers();

const blob = new Blob([buffer], {type: 'text/plain; charset=utf-8'});

blob.text().then(text => console.log(text));

或者

const stringVal = "string here";

const blob = new Blob([stringVal], {type: 'text/plain; charset=utf-8'});

blob.arrayBuffer().then(buffer => console.log(buffer));

你们为什么要把这件事情搞得这么复杂呢?


我认为这在包含二进制数据的字符串情况下不起作用?我认为你需要先将它放入uint8array中。 - Pure Function
啊,在浏览器中,这些内容已经被剥离了。字符串在那里不能容纳任意字节。在 Node 上是否有所不同? - Sean Morris
我不是专家,但浏览器中的一个示例是您可以从Blob接收的USVString。https://developer.mozilla.org/en-US/docs/Web/API/Blob/text - Pure Function
1
如果你从一个 blob 开始,你可能不应该将其转换为字符串再制作缓冲区,因为 blob.arrayBuffer 更直接。 - Sean Morris

19

以下内容都是关于如何从数组缓冲区获取二进制字符串

我建议不要使用

var binaryString = String.fromCharCode.apply(null, new Uint8Array(arrayBuffer));

因为它在处理大型缓冲区时会崩溃(有人提到了“神奇”的246300大小,但我在120000字节的缓冲区上得到了“最大调用栈大小超过”的错误(Chrome 29))

它的性能非常差(见下文)

如果确切需要同步解决方案,请使用类似以下方式

var
  binaryString = '',
  bytes = new Uint8Array(arrayBuffer),
  length = bytes.length;
for (var i = 0; i < length; i++) {
  binaryString += String.fromCharCode(bytes[i]);
}

它的速度与之前的版本一样慢,但功能正常。目前似乎还没有很快的同步解决方案(此主题中提到的所有库都使用了相同的同步特性方法)。

但我真正推荐的是使用Blob + FileReader方法。

function readBinaryStringFromArrayBuffer (arrayBuffer, onSuccess, onFail) {
  var reader = new FileReader();
  reader.onload = function (event) {
    onSuccess(event.target.result);
  };
  reader.onerror = function (event) {
    onFail(event.target.error);
  };
  reader.readAsBinaryString(new Blob([ arrayBuffer ],
    { type: 'application/octet-stream' }));
}
唯一的缺点(并非对所有人都如此)是它是异步的。与以前的解决方案相比,它大约快了8-10倍!(一些细节:在我的环境中,同步解决方案对于2.4Mb缓冲区需要950-1050毫秒,但FileReader的解决方案对于相同数量的数据只需要100-120毫秒。我已经测试了两种同步解决方案在100Kb缓冲区上,它们所花费的时间几乎相同,因此使用“apply”循环不比它们慢多少。)
顺便说一下,在这里:如何将ArrayBuffer转换为字符串并从字符串转换为ArrayBuffer ,作者像我一样比较了两种方法,并得到完全相反的结果(他的测试代码在这里)。为什么结果如此不同?可能是因为他的测试字符串长度为1Kb(他称之为“veryLongStr”)。我的缓冲区是一个真正的大小为2.4Mb的JPEG图像。

16

(更新 请查看本答案的下半部分,我已经(希望)提供了一个更完整的解决方案。)

我也遇到了这个问题,在FF6中以下方法适用于我(仅适用于一个方向):

var buf = new ArrayBuffer( 10 );
var view = new Uint8Array( buf );
view[ 3 ] = 4;
alert(Array.prototype.slice.call(view).join(""));

很不幸地,你最终得到的是数组中值的ASCII文本表示,而不是字符。但这种方法仍然比循环更有效率。

例如,对于上面的示例,结果为0004000000,而不是几个null字符和chr(4)

编辑:

在查看MDC 这里 后,你可以按以下方式从Array创建一个ArrayBuffer

var arr = new Array(23);
// New Uint8Array() converts the Array elements
//  to Uint8s & creates a new ArrayBuffer
//  to store them in & a corresponding view.
//  To get at the generated ArrayBuffer,
//  you can then access it as below, with the .buffer property
var buf = new Uint8Array( arr ).buffer;

回答你的最初问题,这使你可以将ArrayBufferString相互转换:

var buf, view, str;
buf = new ArrayBuffer( 256 );
view = new Uint8Array( buf );

view[ 0 ] = 7; // Some dummy values
view[ 2 ] = 4;

// ...

// 1. Buffer -> String (as byte array "list")
str = bufferToString(buf);
alert(str); // Alerts "7,0,4,..."

// 1. String (as byte array) -> Buffer    
buf = stringToBuffer(str);
alert(new Uint8Array( buf )[ 2 ]); // Alerts "4"

// Converts any ArrayBuffer to a string
//  (a comma-separated list of ASCII ordinals,
//  NOT a string of characters from the ordinals
//  in the buffer elements)
function bufferToString( buf ) {
    var view = new Uint8Array( buf );
    return Array.prototype.join.call(view, ",");
}
// Converts a comma-separated ASCII ordinal string list
//  back to an ArrayBuffer (see note for bufferToString())
function stringToBuffer( str ) {
    var arr = str.split(",")
      , view = new Uint8Array( arr );
    return view.buffer;
}

为了方便,这里提供一个函数,用于将原始的Unicode 字符串转换为ArrayBuffer(仅适用于ASCII / 单字节字符)

function rawStringToBuffer( str ) {
    var idx, len = str.length, arr = new Array( len );
    for ( idx = 0 ; idx < len ; ++idx ) {
        arr[ idx ] = str.charCodeAt(idx) & 0xFF;
    }
    // You may create an ArrayBuffer from a standard array (of values) as follows:
    return new Uint8Array( arr ).buffer;
}

// Alerts "97"
alert(new Uint8Array( rawStringToBuffer("abc") )[ 0 ]);

上述方法允许您将ArrayBuffer转换为String,并再次转换回ArrayBuffer,其中字符串可以存储在例如.localStorage中。

希望这可以帮助到您,

Dan


1
我认为这不是一种高效的方法(无论是时间还是空间),而且这是一种非常不寻常的存储二进制数据的方式。 - kpozin
2
使用base64编码怎么样? - Nick Sotiros

16
与这里的解决方案不同,我需要进行UTF-8数据的转换。为此,我编写了以下两个函数,使用(不)escape /(en)decodeURIComponent技巧。它们在内存上非常浪费,分配的内存为编码的utf8字符串长度的9倍,但应该可以通过垃圾回收来恢复。只是不要将它们用于100MB文本。
function utf8AbFromStr(str) {
    var strUtf8 = unescape(encodeURIComponent(str));
    var ab = new Uint8Array(strUtf8.length);
    for (var i = 0; i < strUtf8.length; i++) {
        ab[i] = strUtf8.charCodeAt(i);
    }
    return ab;
}

function strFromUtf8Ab(ab) {
    return decodeURIComponent(escape(String.fromCharCode.apply(null, ab)));
}

检查它是否正常工作:

strFromUtf8Ab(utf8AbFromStr('latinкирилицаαβγδεζηあいうえお'))
-> "latinкирилицаαβγδεζηあいうえお"

一种广泛支持且性能良好的解决方案:https://jsbench.me/b6klaaxgwq/1 - Finesse

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