先记住这个答案
Uint8Array 不是一种「更快的数组」,而是一块二进制内存的视图。底下是一个 ArrayBuffer,也就是一段固定长度的原始字节;Uint8Array 规定了怎么读它:每个元素 1 个字节、无符号整数。所以它长度固定、没有 push 和 pop,只能存数字,写入超出 0 到 255 的值会被转换。普通数组则可以随时变长、什么值都能放,内存怎么排由引擎自己优化,不承诺连续。
ArrayBuffer是一段字节,Uint8Array是看这段字节的方式- 长度固定,没有
push这类改变长度的方法 - 写入超出范围的值会被转换,
Uint8Array里 300 变成 44 - 多个视图可以共享同一个
ArrayBuffer
长度和类型都是定死的
普通数组的习惯用法在类型化数组上很多都不成立。
- 越界和超范围写入
Uint8Array写入 300,会按 256 取模变成 44;写入-1变成 255。Int8Array写 200 会变成-56。普通数组写 300 就是 300。 越界写入也不一样。普通数组arr[10] = 1会把长度撑到 11;类型化数组bytes[10] = 1什么都不发生,bytes.length还是 4,bytes[10]读出来是undefined。它不会报错,所以写错下标很难发现。- 为什么要这么设计
- 类型固定、内存连续,引擎就能把它直接交给底层用:WebGL 的顶点数据、Web Audio 的采样、
fetch读到的二进制、FileReader读出的文件内容,都是一段字节。普通数组每个元素可能是任意类型,要先转换一遍才能交出去。所以处理图片、音视频、二进制协议时用类型化数组,普通业务数据还是用普通数组。
容易答错的地方
- 以为
Uint8Array是数组 Array.isArray(bytes)是false。它有map和filter这类方法,但改变长度的方法一个都没有。JSON.stringify(new Uint8Array([1, 2]))得到的是{"0":1,"1":2},而不是[1,2],要先Array.from转一下。- 以为
map能返回普通数组 - 类型化数组的
map返回的还是同类型的类型化数组。new Uint8Array([200, 100]).map(x => x * 2)结果是[144, 200],400 被截断成了 144。要保留原值得先转成普通数组再map。 - 以为视图总是覆盖整个
ArrayBuffer new Uint8Array(buffer, 1, 2)只看下标 1 和 2 这两个字节。把它的.buffer传给别的 API,对方拿到的是完整的 4 个字节,可能把不该给的数据也带出去了。要传就传视图本身,或者先slice复制一份。
面试官还会怎么问?
Uint8ClampedArray 和 Uint8Array 有什么区别?
超范围时的处理不同。Uint8Array 取模,300 变成 44;Uint8ClampedArray 夹紧,300 变成 255,-1 变成 0。Canvas 的 getImageData 返回的像素数据就是 Uint8ClampedArray,因为颜色值调亮过头应该是最亮,而不是绕回到很暗。
视图能从任意字节位置开始吗?
不能。byteOffset 必须是元素宽度的整数倍,new Uint16Array(buffer, 1) 会抛 RangeError,因为 2 字节的元素要从偶数位置开始。DataView 没有这个限制,可以从任意位置读多字节的值,这也是解析二进制格式常用它的原因。
类型化数组能传给 Worker 吗?
能,默认会被复制一份。如果数据很大,可以把 buffer 放进 postMessage 的转移列表里,所有权直接转给 Worker,不复制。转移之后原来那边的 buffer 就被分离了,byteLength 变成 0,原视图也读不到数据了。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。