核心回答
先记住这个答案
两个方法的参数一样,都是 (begin, end),不包含 end,但返回的东西性质不同。subarray 返回一个新视图,和原数组共用同一个 ArrayBuffer,只是起点和长度不一样,所以创建很快,但改它就是改原数组。slice 会新建一个 ArrayBuffer,把选中范围复制过去,得到的是独立的副本。判断用哪个,就看这段数据之后会不会被别人改掉。
subarray共享内存,改一边另一边也变slice复制到新的ArrayBuffer,两边独立subarray是O(1),而slice是O(n)- 两者参数都是元素下标,不是字节偏移
复用缓冲区时 subarray 的坑
处理二进制数据时,为了少分配内存,常见做法是准备一个固定的缓冲区反复读写。比如解析网络包,每次都把新数据写进同一个 Uint8Array。
- 存下来的数据被下一次写入覆盖
- 假设每读到一个包,就用
subarray取出包体交给一个异步任务处理。任务还没来得及跑,下一个包已经写进了同一块缓冲区。这时任务拿到的视图指向的内存早就变成了新包的内容,处理的是错的数据,也不会报任何错。 修法是交出去之前slice一份。原则很简单:数据只在当前这一轮同步代码里用,用subarray省一次复制;要存起来、要跨过await、要交给别的模块,用slice。 - 参数是元素下标,不是字节
- 对
Uint32Array调subarray(1),新视图的byteOffset是 4,因为跳过了一个 4 字节的元素。协议文档里写的往往是字节偏移,这时要么先除以元素宽度,要么直接用Uint8Array或DataView按字节操作。
回答前,多想一步
容易答错的地方
- 以为
subarray和普通数组的slice一样 - 普通数组的
slice是浅复制。类型化数组里只有slice是复制,subarray从名字上看像「取子数组」,其实是共享视图,这是最常见的误解。 - 以为
subarray能省内存 - 它确实不复制,但只要这个小视图还被引用着,整个底层
ArrayBuffer都不能被回收。从一个 10MB 的缓冲区里subarray出 10 个字节长期保存,占着的是 10MB。这种情况反而应该slice出来,让大缓冲区能被释放。 - 以为
.buffer相同就代表数据重叠 - 两个视图
.buffer相同,只说明在同一块内存上,不代表它们看的区间有交集。要判断会不会互相影响,还得比较byteOffset和byteLength算出的范围。
试着用自己的话回答
面试官还会怎么问?
subarray 支持负数下标吗?
支持,和数组的 slice 规则一样,负数从末尾往前算。Uint8Array.from([10, 20, 30, 40]).subarray(-2) 得到 [30, 40],它的 byteOffset 是 2。超出范围的下标会被夹到有效范围内,不会报错。
ArrayBuffer 自己的 slice 是复制还是共享?
复制。buffer.slice(begin, end) 按字节范围返回一个新的 ArrayBuffer。ArrayBuffer 没有 subarray,想共享就在它上面新建一个带偏移的视图,比如 new Uint8Array(buffer, 4, 8)。
TextDecoder 解码时传 subarray 会有问题吗?
不会,decode 只读视图覆盖的那一段字节,传 bytes.subarray(0, n) 是解码缓冲区里有效部分的常见写法,而且省掉了一次复制。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。