在 JavaScript 中,何时使用 ArrayBuffer 和 typed array?

68

我正在从Node.js转移到浏览器环境中,并且仍然困惑于ArrayBuffer和类型化数组(例如Uint8Array)之间的区别。

我不确定在哪里使用类型化数组,以及何时直接使用ArrayBuffer。将它们互相转换并不难,但是在什么情况下使用哪一个呢?

例如,当我创建一个代表代码中数据块的对象时,它应该是ArrayBuffer还是Uint8Array?这取决于什么?

或者说:我可否从我的函数中返回ArrayBuffer(例如用于外部API),或者是类型化数组?

请注意,我可以通过 Google 学习如何将元素等添加到这些类型化数组中;我缺少的是一些简短通用的指南,说明何时在哪里使用它们,特别是从 Node.js 的 Buffer 迁移时。


我知道这个问题可能太宽泛了,但我不确定如何更好地提问 :( - Karel Bílek
2
你读过文档了吗?在使用ArrayBuffer时,你永远不会“直接使用”。它是一种抽象,代表着类型化数组背后的存储。 - Pointy
2
但是您可以创建一个ArrayBuffer…var buffer = new ArrayBuffer(8),就像链接中的文档中所示。 - Karel Bílek
我应该从我的函数中返回ArrayBuffer(例如,对于外部API),还是返回类型化数组? - Karel Bílek
非常特殊的情况,例如:当您使用WebGL时,在绘制索引数据时需要使用类型化数组,这种情况下需要使用Float32Array和Uint16Array,请参考:https://developer.mozilla.org/zh-CN/docs/Web/API/WebGL_API/Tutorial/Creating_3D_objects_using_WebGL - Leo
当您需要从特定服务器二进制文件中读取一些数据时(我在一年前创建了这个文件,以减少发送到浏览器的数据),但像@Pointy所说...“这些东西并不是强制性的”。 - Leo
2个回答

130

概念

ArrayBuffers代表物理内存中的字节数组。ArrayBuffer是字节的实际存储位置,但很少直接使用它 - 实际上,您无法直接访问ArrayBuffer的内容,只能传递一个引用。另一方面,它们用于服务器和客户端之间的二进制数据传输,或者通过Blob从用户的文件系统传输。

内存中的ArrayBuffer字节数组
内存中的ArrayBuffer字节数组 - 每个索引等于一个字节。ArrayBuffer在内存中对齐。

要读取ArrayBuffer的内容,您需要使用一个视图。它位于顶部,并提供了一个“api”来按不同宽度类型或任意方式访问字节。

宽度相关的视图

根据您的需求,可以使用不同的视图。如果您只需要读取字节值,即介于-128和127之间的有符号值或介于0和255之间的无符号值,则应使用Int8Array或Uint8Array。请注意,它们的名称有点“误导”,因为它们是视图而不是数组,并且仅引用底层的ArrayBuffer。
同样,您可以查看Int8ArrayUint8ArrayUint8ClampedArrayInt16ArrayUint16ArrayInt32ArrayUint32ArrayFloat32ArrayFloat64Array的视图。
除了*int8Arrays之外,其他的都对ArrayBuffer大小有一些要求。例如,Uint32Array视图必须位于一个可被四整除的ArrayBuffer上,否则会抛出错误。*int16视图需要两字节边界。
通常这不是个问题,因为你可以直接使用视图的构造函数指定索引的数量,并且会自动为它创建一个匹配的ArrayBuffer来满足这些要求。
由于ArrayBuffer是一个字节数组,*int16视图从中读取两个字节 - 或者说一个索引等于两个字节,*int32等于四个字节,依此类推。
Uint8Array和Uint8ClampedArray之间的主要区别在于超出范围的值在普通数组中会进行模运算(例如256变成0),而在夹紧数组中,这些值会被夹紧(256变成255)。
*int16视图

*int32视图
Int32/Uint32和Float32视图-每个索引代表四个字节,并且内存对齐。

Float64视图
Float64视图-每个索引代表八个字节,并且内存对齐。

灵活性的DataView

然后是DataView。这适用于需要灵活的ArrayBuffer并且需要从缓冲区中读取可变宽度和位置的场景,这些位置不一定是宽度或内存对齐的。

例如,*int32索引将始终指向可以被四整除的内存位置。另一方面,DataView可以从位置5读取一个Uint32,并在内部处理所有所需的步骤(位移、掩码等),但会带来一点额外开销。

另一个区别是DataView不使用索引,而是使用绝对字节位置来表示其所代表的数据,并且它具有自己的方法来从任意位置读取或写入各种宽度的数据。

DataView
DataView - 可以从任何位置和任何宽度读取。

在其他情况下,您可以使用几个不同的视图引用相同的底层ArrayBuffer。

目前还没有64位整数的视图,但似乎已提议用于ES8

SharedArrayBuffers

还有一个有用的新功能SharedArrayBuffers,可以在Web Workers之间使用。

过去在某些浏览器中可以使用可传递对象,但SharedArrayBuffers在内存保持不变的意义上更高效,只是关于它的信息被传输。SharedArrayBuffers不能像ArrayBuffers一样分离。

目的和使用领域

类型化数组适合存储特定的数字值,并且速度快。位图是类型化数组的典型候选(例如canvas 2D/WebGL)。

在Web Workers内部进行大量数据处理是另一种用途等等。我已经提到过客户端和服务器之间的二进制传输或文件系统。
DataViews非常适合解析或构建二进制文件和文件格式。
Typed arrays是将二进制数据打包发送到网络、服务器或通过Web套接字和WebRTC的数据通道的绝佳方式。
如果你处理音频、视频、画布或媒体录制,往往无法避免使用Typed arrays。
使用Typed arrays的关键是性能和内存。它们通常在特殊场景中使用,但在普通情况下只需要存储数值(或utf-8字符串、加密向量等)时,也没有问题。它们速度快,占用内存少。
注意事项
有几个注意事项需要注意:
字节顺序
在字节顺序方面必须注意一些预防措施。Typed arrays始终反映它们运行的CPU架构,即小端或大端。大多数消费者系统都是小端,但当使用*int16和*int32数组时,必须特别注意字节顺序。DataView在这方面也可以提供帮助,但如果性能很重要,它并不总是一个好选择。

字节顺序在接收来自服务器的数据时也很重要。它们通常以大端格式(也称为“网络顺序”)存在。对于解析文件格式,同样适用。

浮点数编码

Float32/Float64 将读取和写入使用 IEEE-754 格式编码的数字。如果对同一缓冲区使用了多个视图,这也是需要注意的事项。

跨浏览器支持

现在大多数浏览器都支持类型化数组。如果您必须处理旧版浏览器,那么只有回退到 IE9 或更早版本的移动浏览器才无法使用它们。

Safari 在性能方面并不特别优化,但其他好处还是有的。5.1 版本不支持 Float64。

移动设备有其自身的硬件限制,但通常而言:类型化数组是安全可用的。对于特殊情况,存在一个polyfill


4
感谢你给出了一份精彩的答案。所以,使用ArrayBuffer没有正确的理由,对吧。太好了。 - Karel Bílek
27
基本上,ArrayBuffer 是实际的内存,而 DataView 则是访问以不同大小为单位的内存的包装器,Typed arrayDataView 相同,只是它以固定大小的单位进行访问。谢谢。以下是翻译后的代码示例:const memory = new ArrayBuffer(2); const view = new Uint8Array(memory); view[0] = 0xFF; console.log(new Uint8Array(memory)); // Uint8Array(2) [255, 0] - Константин Ван
6
还有没有其他人跟我一样完全不理解这个,正在寻找其他资源…… - Nambi N Rajan
6
这完全没有回答提问者的问题,即是应该传递和使用 ArrayBuffer 还是像 Uint8Array 那样使用它的视图;是否有使用其中一种方法比另一种更加谨慎的注意事项... - 0__
请注意这些名称有点“误导”,因为它们是视图而不是数组,只引用了底层 ArrayBuffer。这是一个观点吗,还是与旧版规范有关?我知道其中一个构造函数(即 new UintXArray(buffer[, byteOffset[, length]]))创建了一个作为 ArrayBuffer 视图的数组,但其他构造函数则没有。我不知道整个 UintXArray 类是否严格绑定到 ArrayBuffer 的概念上。 - cubuspl42
@cubuspl42,你说得对,UintXArray并不与ArrayBuffer绑定,而且不仅仅是可视的。请参见顶部的示例“从长度”以获取简单的演示:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array#different_ways_to_create_a_uint8array - GViz

3
我更喜欢在函数参数和返回类型中使用TypedArray。TypedArray可以表示ArrayBuffer的一个部分视图,这意味着它的大小可能与底层缓冲区不同。例如:
const buff = new ArrayBuffer(12);

// it will have the bytes from offset 4 to 7 (included)
const arr = new Uint8Array(buff, 4, 4);

数据的部分视图是你函数的关注点,而不是整个底层缓冲区。

如果在这种情况下选择将ArrayBuffer传递给函数,那么你必须创建一个新的ArrayBuffer,这很复杂且会影响性能:

const buff = new ArrayBuffer(12);
foo(new Uint8Array(buff).slice(4, 8).buffer)

function foo(buff: ArrayBuffer) {
}

但是,如果你使用TypedArray,在想要将字节写入存储(如indexedDB)时要小心。确保TypedArray与底层缓冲区对齐,否则可能会将整个数据写入存储器中。
你也可以选择同时接受ArrayBuffer和TypedArray,如果你喜欢的话。
function foo(data: ArrayBufferView | ArrayBuffer) {
  // Convert to a view, or any TypedArray you want
  if(!ArrayBuffer.isView(data)) data = new Uint8Array(data); 
  // ...
}

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