Typed Array 基本介绍
随着 Web 应用的成长越来越快,而且复杂度越来越高,增加了一些新特性:
- 直接对 audio 和 video 进行操作;
- 使用 WebSocket 收发未经过加工的二进制数据。
因此,JavaScript 提供了 Typed Array 类数组对象(也称为类型数组),可以在更深层次或者说更细粒度对数据做控制。Typed Array 具有以下特点:
- 元素为对象的数组,是非常灵活的,可以动态收缩并且可以存放任意 js 类型的数据。因为js引擎会做优化,所以这些数组才会足够快
- Typed Array 不能与普通的 Array 混淆,通过
Array.isArray()
检测 Typed Array 的话,会返回false
- Typed Array 不会继承所有 Array 的方法,例如
push
、pop
不能在 Typed Array 中使用
Typed Array 架构基础:Buffers 和 Views
为了最大程度提高灵活性和有效性,JavaScript 中的 Typed Array 分为 Buffers 和 Views 两种:
- Buffer(通过 ArrayBuffer 类实现)指的是一个数据块对象,没有固定的格式;并且 Buffer 中的内容是不能访问到的。
- 为了获得 Buffer 中内存的访问权限,Typed Array 提供了 View,View 包含一个上下文(包括数据类型,初始位置,元素数量),通过这个上下文将数据转换为 Typed Array。
通过下面这张图可以发现:同一个 Buffer 可以通过不同的 view 来反应数据存储情况:
ArrayBuffer
ArrayBuffer 是一种特殊的数据类型,代表着一个通用的、固定长度的二进制数据缓冲区。
不能直接修改 ArrayBuffer 中的内容,需要通过创建一个 Typed Array 的 view 或者 DataView 去修改。因为 view 或者 DataView 是一种特殊格式的 buffer,从而读写 buffer 中的内容。
Typed Array views
Typed Array views 有自描述名字,为 Int8
,Uint32
,Float64
等多种常见数字类型提供了 view。
有一种特殊的 Typed Array view - Uint8ClampedArray
,它将数值严格限制在 0 到 255 之间,这个类型在 Canvas Data Processing 方面很有用,因为 Canvas 的 getImageData()
返回 ImageData,而 ImageData 实例是由 Uint8ClampedArray
和 image 的大小组成,在 node 标准库常用语法中有相关资料。
目前主要有 11 种 Typed Array view,不同类型的 Typed Array views,主要在 4 个方面有差异:Value Range,Size in bytes,Web IDL types 以及等价的 C 语言中的数据类型。
例如:Int8Array Typed Array view,Value Range 是 -128 到 127,字节大小是 1 byte;Web IDL type 是 byte,等价 C 语言中的 int8_t 类型。
扩展知识:什么是 Web IDL type
IDL 是 Interface Definition Language 的简称,用来描述打算在 Web 浏览器中实现的 interface。
DataView
DataView 是一个更底层的接口,它提供了读写缓冲区中二进制数据的 getter
和 setter
方法。
DataView 用来处理不同类型数据很有用,例如 Typed Array views 采用平台的原生字节顺序(可以查看 Endianness)但是通过 DataView,我们可以调整字节顺序,默认情况字节顺序是大端模式,可以通过 DataView 的 api 调整为小端模式。
ArrayBuffer 的构造函数 DataView(buffer [, byteOffset [, byteLength]])
,其中:buffer
是 ArrayBuffer 实例,byteOffset
是新 DataView 的偏移量,byteLength
是新 DataView 的字节长度。
// create an ArrayBuffer with a size in bytes
var buffer = new ArrayBuffer(16);
// Create a couple of views
var view1 = new DataView(buffer);
var view2 = new DataView(buffer,12,4); //from byte 12 for the next 4 bytes
view1.setInt8(12, 42); // put 42 in slot 12
console.log(view1.buffer);
console.log(view2.buffer);
console.log(view1.getInt8(12));
console.log(view2.getInt8(0));
扩展知识:什么是 Endianness?
Endianness 是指内存存储的大小端模式。
- 大端模式,指的是低位数据高地址,0x12345678,12存buf[0],78(低位数据)存buf[3](高地址),也就是常规的正序存储。
- 小端模式与大端模式相反。0x12345678,78 存在 buf[0],存在低地址。
Typed Arrays 的 Web API
使用了 Typed Array 的 Web API 有以下这些:
FileReader.prototype.readAsArrayBuffer()
之前接触过 FileReader 的 readAsDataURL()
,该方法用于读取 Blob 或者 File 对象,在 reader.result
上输出 base64 格式的字符串。readAsArrayBuffer()
方法也是读取 File 或者 Blob 对象的内容。
和 readAsDataUR()
类似,读取 Blob 或者 File 对象完毕后,readyState
值变为 DONE
,同时触发 loadend
事件,在 reader.result
属性上包含代表 file data 的 ArrayBuffer。也可以直接用 blob.arrayBuffer()
得到 blob 的二进制数据并且以 Promise 的方式处理。
blob.arrayBuffer().then(buffer => /* 处理ArrayBuffer */)
XMLHttpRequest.prototype.send()
该方法支持发送 Typed Array 和 ArrayBuffer 对象作为入参。
方法 XMLHttpRequest.send(body) 中的 body 参数支持多种类型的数据,包括:
- 序列化之后的 Document
- BodyInit:包括 Blob,BufferSource,FormData,URLSearchParams,ReadableStream 或者 USVString object。
规范中写明了其算法:https://fetch.spec.whatwg.org/#bodyinit。其中有一步是设置对象类型,切换 Content-Type 为对象的 MIME 类型。
IDL 的定义如下:
interface mixin Body {
readonly attribute ReadableStream? body;
readonly attribute boolean bodyUsed;
[NewObject] Promise<ArrayBuffer> arrayBuffer();
[NewObject] Promise<Blob> blob();
[NewObject] Promise<FormData> formData();
[NewObject] Promise<any> json();
[NewObject] Promise<USVString> text();
};
通过 send
方法发送二进制数据的最好方式是:ArrayBufferView 或 Blob。
IDL 通常用 JavaScript 或者 Java 实现。此处的 interface mixin 是 Java 实现。
如何将 Typed Array 转换为普通数组?
在处理完 Typed Array 以后,很多情况下会将其转换为普通数组。
可以使用 Array.from()
方法把 Typed Array 转换成普通数组:
let typedArray = new Uint8Array([1, 2, 3, 4]);
let normalArray = Array.from(typedArray);
或者,使用以下代码转换:
let typedArray = new Uint8Array([1, 2, 3, 4]),
normalArray = Array.prototype.slice.call(typedArray);
normalArray.length === 4;
normalArray.constructor === Array;
Typed Array 使用例子
1、使用带 buffer(缓冲区)的 view(视图)
// 创建一个 16 字节定长的 buffer
let buffer = new ArrayBuffer(16);
这里用到了通信原理中的 1byte = 8bit(1 字节等于 8 个比特)
上图中的 Int8
,Int16
,Int32
,Uint8
中的数字都是以 bit 为单位的,其总空间为 16 * 8 = 128bit。
因此:
- Int8Array 和 Uint8Array 类型的 ArrayBuffer 长度为16;
- Int16Array 类型的 ArrayBuffer 长度为 8
- Int32Array 类型的 ArrayBuffer 长度为 4
位数越大,说明 ArrayBuffer 中的一个 0 占用的内存空间越大。
// 通过 byteLength 属性,可以获得一个 buffer 占用的字节内存空间
console.log(buffer.byteLength); // 将输出 16
// 以下代码会创建一个将 buffer 中的数据转换为 32bit 的有符号整型数组
let int32View = new Int32Array(buffer);
这里除了可以创建 Int32Array 的 ArrayBufferView,还可以创建 Int8Array,Uint8Array,Int16Array 等等类型的 ArrayBufferView。
// 可以像普通数组一样去遍历 ArrayBufferView 数组
int32View.forEach((e, i, arr) => { arr[i] = i; }) // 0, 1, 2, 3
这个数组有 4 个 entry,每个 entry 占用 4 个字节,所以总计 16 个字节。
2、处理复杂的数据结构
用多个不同类型的 view 联接 1 个单独的 buffer,需要从一个 buffer 的不同偏移量开始。
可以与一个包含了多数据类型的 Data Object 交互。例如 WebGL 交互复杂数据结构、data files 或者 C 数据结构。
struct someStruct {
unsigned long id; // long 32bit
char username[16]; // char 8bit
float amountDue; // float 32bit
};
上面的 C 结构在 js 中可以这样定义:
let buffer = new ArrayBuffer(24);
// ... read the data into the buffer ...
let idView = new Uint32Array(buffer, 0, 1);
let usernameView = new Uint8Array(buffer, 4, 16);
let amountDueView = new Float32Array(buffer, 20, 1);
需要注意 C 语言的数据结构是与平台相关的。因此,C 语言中的 long,char,float 具体占用多少 bit,与运行平台有关。
扩展知识:
1、
Uint32Array(buffer, 0, 1)
,其中的 0,1 代表什么?
- 0 代表从 ArrayBuffer 开始位置的偏移量。构造时固定,且只读。
- 1 表示 ArrayBuffer 中元素的个数。构造时固定,只读。
2、length 与 byteLength 区别是什么?
直接看代码:
var buffer = new ArrayBuffer(16); var int32View = new Int32Array(buffer); int32View.length; // 4 int32View.byteLength; // 16