现代 JavaScript 教程:手写 `concat` 函数拼接 `Uint8Array` 类型化数组

发布时间:2026/10/9 5:15:29
现代 JavaScript 教程:手写 `concat` 函数拼接 `Uint8Array` 类型化数组
文档教程前端【免费下载链接】zh.javascript.info现代 JavaScript 教程The Modern JavaScript Tutorial以最新的 ECMAScript 规范为基准通过简单但足够详细的内容为你讲解从基础到高阶的 JavaScript 相关知识。项目地址https://gitcode.com/gh_mirrors/zh/zh.javascript.info点击查看免费下载导读在 Web 开发中二进制数据文件上传、下载、图像处理几乎无处不在而 JavaScript 处理二进制数据的核心载体就是ArrayBuffer及其上的类型化数组视图TypedArray。然而类型化数组并不像普通Array那样自带concat方法本文基于现代 JavaScript 教程仓库中 4-binary/01-arraybuffer-binary-arrays 章节的实战练习对应任务见 task.md完整讲解如何手写一个concat(arrays)函数把多个Uint8Array片段拼接成单一数组。读完本文你将掌握类型化数组的set批量拷贝机制、reduce统计长度的方法以及如何基于BYTES_PER_ELEMENT理解底层内存布局并能独立解决此类二进制数据拼接需求。为什么需要手写concatTypedArray 方法的缺失普通数组Array拥有丰富的方法但类型化数组TypedArray即Uint8Array、Uint16Array、Float64Array等视图的统称与普通数组存在两个显著差异。在 article.md 的 TypedArray 方法 一节中明确指出没有splice无法删除一个值因为类型化数组是缓冲区buffer上的视图而缓冲区是固定的、连续的内存区域我们所能做的只是分配一个零值没有concat方法这正是本练习需要手写concat的直接原因。类型化数组虽然支持map、slice、find、reduce等常规数组方法但针对拼接场景它提供了两个关键替代工具arr.set(fromArr, [offset])从offset默认为 0开始将fromArr中的所有元素复制到arrarr.subarray([begin, end])创建从begin到end不包括的同类型新视图与slice类似但不复制任何内容只是对同一段数据开一个新窗口。其中set方法就是实现拼接的核心它让我们能够把多个来源数组依次写入一个预先分配好的目标数组。任务要求把多个 Uint8Array 片段拼成一个练习的任务描述非常简洁见 task.md给定一个Uint8Array数组请写一个函数concat(arrays)将数组拼接成一个单一数组并返回。配套的任务模板source.js给出了调用示例function concat(arrays) { // ...your code... } let chunks [ new Uint8Array([0, 1, 2]), new Uint8Array([3, 4, 5]), new Uint8Array([6, 7, 8]) ]; console.log(Array.from(concat(chunks))); // 0, 1, 2, 3, 4, 5, 6, 7, 8 console.log(concat(chunks).constructor.name); // Uint8Array从模板可以提炼出两条隐含验收标准内容正确拼接结果应为[0, 1, 2, 3, 4, 5, 6, 7, 8]类型保持返回值的构造器必须是Uint8Array即结果仍是类型化数组而不是普通数组。解题思路先算总长再依次 set 拷贝实现concat的核心思路分三步统计总长度用reduce累加所有子数组的length得到结果数组所需的元素个数预分配结果数组用new Uint8Array(totalLength)一次性分配内存元素个数 × 每元素字节数BYTES_PER_ELEMENT 底层ArrayBuffer的字节数逐个拷贝维护一个游标length对每个子数组调用result.set(array, length)把内容写入目标数组的指定偏移位置然后让游标累加该子数组的长度保证下一段紧接上一段之后。这里不需要考虑字节对齐或元素大小换算——因为输入输出都是同一类型Uint8Array每个元素恰好占 1 个字节元素个数就是字节偏移量若换成Uint16Array底层set依然按元素为单位拷贝偏移量同样是元素索引而非字节索引。官方参考解法与逐行解读仓库中给出了官方解法完整代码见 _js.view/solution.jsfunction concat(arrays) { // sum of individual array lengths let totalLength arrays.reduce((acc, value) acc value.length, 0); let result new Uint8Array(totalLength); if (!arrays.length) return result; // for each array - copy it over result // next array is copied right after the previous one let length 0; for(let array of arrays) { result.set(array, length); length array.length; } return result; }逐段解读let totalLength arrays.reduce((acc, value) acc value.length, 0);第 3 行reduce从初始值 0 开始累加每个子数组的length。类型化数组与普通数组一样有length属性因此这一步对Uint8Array、Uint16Array等任何类型化数组都通用。let result new Uint8Array(totalLength);第 5 行创建长度元素个数为totalLength的新类型化数组。它会在内部自动创建对应字节数的ArrayBuffer并预填充 0。这是典型的先分配、后填充模式避免在循环中反复扩容。if (!arrays.length) return result;第 7 行空输入时直接返回空数组。这既是一种短路优化也让空输入行为变得明确——返回长度为 0 的Uint8Array而不会因为循环不执行而出现其他意外。result.set(array, length); length array.length;第 13-14 行这是拼接的核心。set方法把当前子数组整体拷贝到结果数组的length偏移处随后游标前进当前子数组的长度使下一段拷贝紧接其后等价于完成了串行拼接。注意result.set的语义是元素级批量拷贝它把源数组的每个元素值复制到目标数组而不是共享底层内存。因此即使后续修改chunks中的原始片段也不会影响已拼接好的result。测试用例验证类型、内容与空输入仓库为本题编写了完整的测试_js.view/test.js覆盖三个关键行为describe(concat, function() { let chunks [ new Uint8Array([0, 1, 2]), new Uint8Array([3, 4, 5]), new Uint8Array([6, 7, 8]) ]; it(result has the same array type, function() { let result concat(chunks); assert.equal(result.constructor, Uint8Array); }); it(concatenates arrays, function() { let result concat(chunks); assert.deepEqual(result, new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7, 8])); }); it(returns empty array on empty input, function() { let result concat([]); assert.equal(result.length, 0); }); });三个用例分别验证类型保持result.constructor Uint8Array。由于我们是用new Uint8Array(totalLength)构造结果返回的自然是Uint8Array如果误用Array.from或其他方式返回普通数组此用例就会失败。拼接正确性三个片段[0,1,2]、[3,4,5]、[6,7,8]依次拼成[0..8]assert.deepEqual确保元素值逐位相等。空输入健壮性concat([])返回长度为 0 的类型化数组对应解法中if (!arrays.length) return result;的分支。底层原理从视图理解拼接的本质要真正理解这段代码需要回到类型化数组的本质。ArrayBuffer是对固定长度连续内存区域的引用它本身不携带任何类型信息而Uint8Array等视图对象就是眼镜透过它把ArrayBuffer中的字节解释为特定格式的数字序列——Uint8Array把每个字节视为 0~255 的 8 位无符号整数见 article.md同一块 ArrayBuffer 可以通过不同视图Uint8Array、Uint16Array、Uint32Array、Float64Array解释为不同粒度的数字序列这也是视图不存储数据、只负责解释数据的含义。由此可以推出拼接实现的两个关键结论分配语义new Uint8Array(totalLength)与new ArrayBuffer(totalLength)的区别在于前者自动创建了配套的ArrayBuffer视图除传入ArrayBuffer的第一种构造变体外其他构造方式都会自动创建底层 buffer详见 article.md。result.byteLength恰好等于totalLength × Uint8Array.BYTES_PER_ELEMENT即 1因此元素个数与字节数在本题中数值相等。拷贝语义result.set(array, offset)是 TypedArray 原生方法按元素逐个复制值。这正是固定长度缓冲区上无法 splice/无法 concat这一限制的补偿手段——我们无法原地增删但可以预分配一块足够大的新缓冲区再把旧数据搬进去。扩展思考泛化与边界情况本题的官方解法已足够精炼但在实际工程中还可做如下泛化思考时不妨对照 article.md 中完整的类型化数组清单Int8Array、Uint16Array、Uint32Array、Float32Array、Float64Array等支持任意类型的输入数组若arrays中混有Uint8Array和Uint16Arrayset会在拷贝时自动做类型转换超出目标类型范围的高位会被截断参见 article.md 的越界行为一节。是否需要按最大类型统一结果取决于业务语义。把结果类型参数化可将解法封装为concat(arrays, TypedArrayClass Uint8Array)用new TypedArrayClass(totalLength)构造结果以支持拼接任意类型化数组。大数据量下的性能set是引擎级实现的批量拷贝比逐元素result[i] array[i]循环更快这是官方解法选择set的根本原因。总结类型化数组没有内建的concat方法但借助reduce统计总长度 new TypedArray(totalLength)预分配 set(array, offset)批量拷贝三步即可高效实现多段二进制数据的拼接。本文给出的官方解法solution.js及其配套测试test.js完整覆盖了类型保持、内容正确、空输入三个关键场景可作为后续处理Blob、File分片合并或网络流数据重组时的直接参考。更完整的ArrayBuffer、TypedArray 与DataView知识体系可继续阅读 4-binary/01-arraybuffer-binary-arrays/article.md。赞分享文档教程前端【免费下载链接】zh.javascript.info现代 JavaScript 教程The Modern JavaScript Tutorial以最新的 ECMAScript 规范为基准通过简单但足够详细的内容为你讲解从基础到高阶的 JavaScript 相关知识。项目地址https://gitcode.com/gh_mirrors/zh/zh.javascript.info点击查看免费下载相关推荐JavaScript TypedArray 拼接实战为二进制数组手写 concat 函数JavaScript TypedArray 拼接实战为二进制数组手写 concat 函数 导读 本篇文章基于《现代 JavaScript 教程》中文仓库 4文档教程前端手写 Uint8Array 拼接函数en.javascript.info 中的 TypedArray concat 实战任务解析手写 Uint8Array 拼接函数en.javascript.info 中的 TypedArray concat 实战任务解析 本篇技术指南以 The Mo文档/教程前端type-challenges 第 533 题 Concat在类型系统里实现数组拼接type challenges 第 533 题 Concat在类型系统里实现数组拼接 导读 本指南围绕 type challenges 仓库中的第 533 道示例工程上一篇Rust pcap未来展望即将到来的令人兴奋的新特性下一篇5个ffmpeg-python帧操作技巧视频编辑入门完全指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考