先记住这个答案
类型化数组(比如 Uint16Array)构造时就定死了元素类型,之后每个下标都按同样的宽度、按本机字节序去读,而且视图的起始偏移必须是元素宽度的整数倍。二进制协议往往是 1 字节类型、2 字节大端长度、4 字节小端 ID 紧挨着排,宽度不一样,字节序也不一样,还不对齐。DataView 正好解决这三件事:任意偏移、任意宽度,每次调用 getUint16(offset, littleEndian) 都能自己指定字节序。
DataView每次读写都能单独指定字节序DataView默认按大端读,小端要传true- 类型化数组用本机字节序,偏移必须是元素宽度的倍数
- 同类数值批量计算用类型化数组,混合字段用
DataView
解析一个真实的协议头
假设服务端通过 WebSocket 发来一段二进制消息,协议文档写着:第 0 字节是消息类型,第 1~2 字节是大端的长度,第 3~6 字节是小端的用户 ID。用 DataView 照着文档一行一行读就行。
// 1 字节类型 + 2 字节长度(大端)+ 4 字节 ID(小端)
const bytes = Uint8Array.from([0x01, 0x00, 0x10, 0x78, 0x56, 0x34, 0x12])
const view = new DataView(bytes.buffer)
const type = view.getUint8(0) // 单字节,不涉及字节序
const length = view.getUint16(1) // 不传第二个参数就是大端
const id = view.getUint32(3, true) // 传 true 按小端读
console.log(type, length, id.toString(16))查看输出与解释
1 16 12345678length 从偏移 1 开始读两个字节 00 10,大端把前一个字节当高位,结果是 16。id 读的是 78 56 34 12,小端把最后一个字节当高位,拼出来就是 0x12345678。偏移 1 和偏移 3 都不是 2 或 4 的倍数,DataView 照样能读。
大端和小端可以这样记:大端是「高位在前」,跟我们写数字的习惯一样;小端是「低位在前」,x86 和绝大多数 ARM 设备的内存就是这么存的。网络协议传统上用大端,所以也叫网络字节序。
类型化数组卡在哪两件事上
类型化数组不是不能读二进制,而是它假设整块内存都是同一种元素,而且是本机写出来的。拿它去读外部来的数据,就会碰到字节序和对齐两个问题。
const buf = Uint8Array.from([0x01, 0x02, 0x03, 0x04]).buffer
const u16 = new Uint16Array(buf)
console.log(u16[0].toString(16)) // 按本机字节序读,小端机器上是 0x0201
const view = new DataView(buf)
console.log(view.getUint16(0).toString(16)) // DataView 默认大端,是 0x0102
try {
new Uint16Array(buf, 1) // 起始偏移 1 不是 2 的倍数
} catch (err) {
console.log(err.name)
}查看输出与解释
201
102
RangeError同样两个字节 01 02,用 Uint16Array 在小端机器上读成 0x0201,而 DataView 默认读成 0x0102。如果协议规定的是大端,用类型化数组就读错了,而且换到大端机器上结果还会变。
第二个问题是对齐。Uint16Array 的起始偏移必须是 2 的倍数,Uint32Array 必须是 4 的倍数,否则构造时直接抛 RangeError。紧凑协议里字段一个挨一个,根本不保证对齐。
- 什么时候反而该用类型化数组
- 数据是本机自己产生、自己消费的同质数组时,类型化数组更合适。比如 Canvas 的
ImageData.data是Uint8ClampedArray,Web Audio 的采样是Float32Array,WebGL 的顶点数据也是类型化数组。这些场景要的是批量按下标读写,速度快,字节序也不用管,因为从头到尾都在同一台机器上。
容易答错的地方
- 以为
DataView默认是小端 - 多字节方法的
littleEndian参数省略时是false,也就是按大端读。很多人因为电脑是小端机,就以为DataView默认小端。协议是小端时一定要显式传true,别靠记忆。 - 把字节序理解成字节数组被反转了
- 换字节序不会动内存里的任何一个字节,变的只是读多字节数值时谁算高位。同一个
ArrayBuffer可以同时被一个大端读取、一个小端读取,互不影响。 - 读之前不检查长度
DataView不认识你的协议,读超出 buffer 范围时直接抛RangeError。解析网络包要先校验最小长度,再用头部里声明的长度限制后面的偏移,否则截断的包或者恶意伪造的长度字段会让解析崩掉。
面试官还会怎么问?
读单字节字段需要传字节序参数吗?
不需要。字节序只影响多个字节怎么组合成一个数,getUint8 和 getInt8 只读一个字节,所以它们根本没有 littleEndian 参数。
怎么判断当前机器是大端还是小端?
用同一块内存写一个多字节数再按字节读回来:new Uint8Array(new Uint16Array([1]).buffer)[0] === 1 为 true 就是小端。实际项目里几乎不需要判断,因为用 DataView 显式指定字节序,就和机器无关了。
浮点数和 64 位整数也能指定字节序吗?
可以。DataView 有 getFloat32 和 getFloat64,还有返回 BigInt 的 getBigInt64 和 getBigUint64,对应的 set 方法也一样,第二个或第三个参数就是 littleEndian。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。