JavaScript类型化数组是一种类似数组的对象,并提供了一种用于访问原始二进制数据的机制。 正如你可能已经知道,Array 存储的对象能动态增多和减少,并且可以存储任何JavaScript值。JavaScript引擎会做一些内部优化,以便对数组的操作可以很快。然而,随着Web应用程序变得越来越强大,尤其一些新增加的功能例如:音频视频编辑,访问WebSockets的原始数据等,很明显有些时候如果使用JavaScript代码可以快速方便地通过类型化数组来操作原始的二进制数据将会非常有帮助。
但是,不要把类型化数组与正常数组混淆,因为在类型数组上调用 Array.isArray() 会返回false。此外,并不是所有可用于正常数组的方法都能被类型化数组所支持(如 push 和 pop)。
对于前端程序员来说,平时很少和二进制数据打交道,所以基本上用不到ArrayBuffer,大家对它很陌生,但是在使用WebGL的时候,ArrayBuffer无处不在。浏览器通过WebGL和显卡进行通信,它们之间会发生大量的、实时的数据交互,对性能的要求特别高,它们之间的数据通信必须是二进制的才能满足性能要求,而不能是传统的文本格式。文本格式传递一个 32 位整数,两端的 JavaScript 脚本与显卡都要进行格式转化,将非常耗时。类型化数组的诞生就是为了能够让开发者通过类型化数组来操作内存,大大增强了JavaScript处理二进制数据的能力。
为了达到最大的灵活性和效率,JavaScript 类型数组(Typed Arrays)将实现拆分为缓冲和视图两部分。一个缓冲(由 ArrayBuffer 对象实现)描述的是一个数据块。缓冲没有格式可言,并且不提供机制访问其内容。为了访问在缓冲对象中包含的内存,你需要使用视图。视图提供了上下文 — 即数据类型、起始偏移量和元素数 — 将数据转换为实际有类型的数组。缓冲和视图的工作方式如下图所示:
var buffer = new ArrayBuffer(16);
if (buffer.byteLength === 16) { console.log("Yes, it's 16 bytes."); } else { console.log("Oh no, it's the wrong size!"); }
var int32View = new Int32Array(buffer);
for (var i = 0; i < int32View.length; i++) { int32View[i] = i * 2; }
该代码会将数组以0, 2, 4和6填充 (一共4个4字节元素,所以总长度为16字节)。
完整代码如下
// 创建一个16字节长度的缓冲 var buffer = new ArrayBuffer(16); // 创建一个视图,此视图把缓冲内的数据格式化为一个32位(4字节)有符号整数数组 var int32View = new Int32Array(buffer); // 我们可以像普通数组一样访问该数组中的元素 for (var i = 0; i < int32View.length; i++) { int32View[i] = i * 2; } // 运行完之后 int32View 为[0,2,4,6] // 创建另一个视图,此视图把缓冲内的数据格式化为一个16位(2字节)有符号整数数组 var int16View = new Int16Array(buffer); for (var i = 0; i < int16View.length; i++) { console.log(int16View[i]); } // 打印出来的结果依次是0,0,2,0,4,0,6,0
那么,这样呢?
int16View[0] = 32; console.log("Entry 0 in the 32-bit array is now " + int32View[0]); // 这次的输出是"Entry 0 in the 32-bit array is now 32"。也就是,这2个数组都是同一数据的以不同格式展示出来的视图。你可以使用任何一种 view types 中的定义的视图。
以上代码看不明白,请结合图和字节数观察
在WebGL中有这么一个需求,我要绘制一个带颜色的三角形,这个三角形有三个顶点,每个点有3个坐标和一个RGBA颜色,现在有了三角形的顶点和颜色数据,需要创建一个缓冲,把三角形的数据按顺序填入,然后传输给WebGL。目前的三角形数据是这样的:
var triangleVertices = [ // (x, y, z) (r, g, b, a) 0.0, 0.5, 0.0, 255, 0, 0, 255, // V0 0.5, -0.5, 0.0, 0, 250, 6, 255, // V1 -0.5, -0.5, 0.0, 0, 0, 255, 255 // V2 ];
目标格式是一个ArrayBuffer,它的格式是这样的:
表示坐标的浮点数是32位的,占4个字节,表示颜色的正整数是8位的,占1个字节,因此我们需要创建两个视图来对这个缓冲进行赋值。
var triangleVertices = [ // (x, y, z) (r, g, b, a) 0.0, 0.5, 0.0, 255, 0, 0, 255, // V0 0.5, -0.5, 0.0, 0, 250, 6, 255, // V1 -0.5, -0.5, 0.0, 0, 0, 255, 255 // V2 ]; var nbrOfVertices = 3; // 顶点数量 var vertexSizeInBytes = 3 * Float32Array.BYTES_PER_ELEMENT + 4 * Uint8Array.BYTES_PER_ELEMENT; // 一个顶点所占的字节数 3*4+4*1 = 16 var buffer = new ArrayBuffer(nbrOfVertices * vertexSizeInBytes); // 3 * 16 = 48 三个顶点一共需要的字节数 var positionView = new Float32Array(buffer); var colorView = new Uint8Array(buffer); var positionOffsetInFloats = 0; var colorOffsetInBytes = 12; var k = 0; // 用三角形数据填充arrayBuffer for (var i = 0; i < nbrOfVertices; i++) { positionView[positionOffsetInFloats] = triangleVertices[k]; // x positionView[1 + positionOffsetInFloats] = triangleVertices[k + 1]; // y positionView[2 + positionOffsetInFloats] = triangleVertices[k + 2]; // z colorView[colorOffsetInBytes] = triangleVertices[k + 3]; // r colorView[1 + colorOffsetInBytes] = triangleVertices[k + 4]; // g colorView[2 + colorOffsetInBytes] = triangleVertices[k + 5]; // b colorView[3 + colorOffsetInBytes] = triangleVertices[k + 6]; // a positionOffsetInFloats += 4; // 4个字节的浮点数循环一次要偏移4位 colorOffsetInBytes += 16; // 1个字节的整数循环一次要偏移16位 k += 7; // 原数组一次处理七个数值(三个坐标四个颜色) }
类型化数组的内容还有很多,在这里我只重点介绍了一下缓冲和视图是如何一起合作来管理内存的。
类型化数组的出现最大的作用就是提升了数组的性能,js中Array的内部实现是链表,可以动态增大减少元素,但是元素多的话,性能会比较差,类型化数组管理的是连续内存区域,知道了这块内存的起始位置,可以通过起始位置+N * 偏移量(一次加法一次乘法操作)访问到第N个位置的元素,而Array的话就需要通过链表一个一个的找下去。
类型化数组的使用场景并不多,可以说是为WebGL量身定做的,不过还是希望你能在以后遇到大量数据的场景能够想起来JS的类型化数组这个功能。