{T}

定型数组

定型数组(Typed Array)是 JavaScript 处理二进制数据的核心技术,广泛应用于 WebGL、Canvas、Web Audio、文件处理等场景。本章将深入讲解定型数组的原理、API和实战应用。

本章概览

定型数组是 ECMAScript 新增的结构,目的是提升向原生库传输数据的效率。它提供了一种更高效的二进制数据处理方式。

code
定型数组体系
├── 基础概念
│   ├── 历史背景 - WebGL 性能优化需求
│   ├── ArrayBuffer - 二进制数据缓冲区
│   └── 视图概念 - DataView vs 定型数组
├── 核心组件
│   ├── ArrayBuffer - 内存分配和管理
│   ├── DataView - 灵活的数据读写
│   └── 定型数组类型 - 9种类型化数组
├── 定型数组类型
│   ├── 整数类型 - Int8/16/32, Uint8/16/32
│   ├── 浮点类型 - Float32/64
│   └── 特殊类型 - Uint8ClampedArray
├── 内存管理
│   ├── 字节序 - 大端/小端字节序
│   ├── 溢出处理 - 数值溢出的自动处理
│   └── 性能优化 - 内存布局和访问效率
└── 实际应用
    ├── WebGL - 图形渲染
    ├── Canvas - 图像处理
    ├── Web Audio - 音频数据
    ├── 文件处理 - 二进制文件读写
    └── 网络协议 - 二进制协议解析

相关章节导航

为什么需要定型数组?

性能对比

javascript
// 性能测试:普通数组 vs 定型数组
const size = 1000000

// 普通数组
console.time('普通数组')
const normalArray = new Array(size).fill(0).map(() => Math.random())
const normalSum = normalArray.reduce((a, b) => a + b, 0)
console.timeEnd('普通数组')

// 定型数组
console.time('定型数组')
const typedArray = new Float32Array(size).map(() => Math.random())
const typedSum = typedArray.reduce((a, b) => a + b, 0)
console.timeEnd('定型数组')

// 结果:定型数组通常快 2-5 倍

内存占用对比

code
普通数组内存布局:
┌─────────────────────────────────────────┐
│ 元素0: 对象引用 (8字节)                   │
│ 元素1: 对象引用 (8字节)                   │
│ 元素2: 对象引用 (8字节)                   │
│ ...                                      │
│ 每个元素都是对象引用,内存占用大           │
└─────────────────────────────────────────┘

定型数组内存布局 (Int32Array):
┌─────────────────────────────────────────┐
│ 元素0: 4字节 │ 元素1: 4字节 │ 元素2: 4字节 │
│ 连续内存,无引用,CPU缓存友好             │
└─────────────────────────────────────────┘
特性普通数组定型数组
内存占用较大(对象引用)较小(连续内存)
访问速度慢(需要解引用)快(直接访问)
类型安全无(可存储任意类型)有(固定类型)
适用场景通用数据存储数值计算、二进制处理
大小调整可动态调整固定大小

历史

随着浏览器的流行,会通过它来运行复杂的 3D 应用程序。早在 2006 年,MozillaOpera 等浏览器提供商就实验性地,在浏览器中增加用于渲染复杂图形应用程序的编程平台,无须安装任何插件。其目标是开发一套 JavaScript API,从而充分利用 3D 图形 API 和 GPU 加速,以便在 <canvas> 元素上渲染复杂的图形

最后的 JavaScript API 是基于 OpenGL ES(OpenGL for Embedded Systems)2.0 规范的。OpenGL ES 是 OpenGL 专注于 2D 和 3D 计算机图形的子集。这个新 API 被命名为 WebGL(Web Graphics Library) ,于 2011 年发布 1.0 版。有了它,开发者就能够编写涉及复杂图形的应用程序,它会被兼容 WebGL 的浏览器原生解释执行

在 WebGL 的早期版本中,因为 JavaScript 数组与原生数组之间不匹配,所以出现了性能问题。图形驱动程序 API 通常不需要以 JavaScript 默认双精度浮点格式传递给它们的数值,而这恰恰是 JavaScript 数组在内存中的格式。因此,每次 WebGLJavaScript 运行时之间传递数组时,WebGL 绑定都需要在目标环境分配新数组,以其当前格式迭代数组,然后将数值转型为新数组中的适当格式,而这些要花费很多时间

⚠️ 这当然是难以接受的,Mozilla 为解决这个问题而实现 CanvasFloatArray。这是一个提供 JavaScript 接口的、C 语言风格的浮点值数组。JavaScript 运行时使用这个类型可以分配、读取和写入数组。 这个数组可以直接传给底层图形驱动程序 API,也可以直接从底层获取到。最终 CanvasFloatArray 变成 Float32Array,也就是今天定型数组中可用的第一个“类型”

ArrayBuffer

Float32Array 实际上是一种“视图”,可以允许 JavaScript 运行时访问一块名为 ArrayBuffer 的预分配内存。ArrayBuffer 是所有定型数组及视图引用的基本单位。

ArrayBuffer 是一个普通的 JavaScript 构造函数,可用于在内存中分配特定数量的字节空间

javascript
const buf = new ArrayBuffer(16) // 在内存中分配 16 字节

console.log(buf.byteLength) // 16

ArrayBuffer 一经创建就不能再调整大小。但可以使用 slice 复制其全部或部分到一个新实例中:

javascript
const buf1 = new ArrayBuffer(16)
const buf2 = buf1.slice(4, 12)
console.log(buf2.byteLength) // 8

要读取或写入 ArrayBuffer,就必须通过视图。视图有不同的类型,但引用的都是 ArrayBuffer 中存储的二进制数据

DataView

第一种允许读写 ArrayBuffer 的视图是 DataView。这个视图专为文件 I/O 和网络 I/O 设计,其 API 支持对缓冲数据的高度控制,但相比于其他类型的视图性能也差一些。DataView 对缓冲内容没有任何预设,也不能迭代。

必须在对已有的 ArrayBuffer 读取或写入时才能创建 DataView 实例。这个实例可以使用全部或部分 ArrayBuffer,且维护着对该缓冲实例的引用,以及视图在缓冲中开始的位置

javascript
const buf = new ArrayBuffer(16)

// DataView 默认使用整个 ArrayBuffer
const fullDataView = new DataView(buf)
console.log(fullDataView.byteOffset) // 0
console.log(fullDataView.byteLength) // 16
console.log(fullDataView.buffer === buf) // true

// 构造函数接收一个可选的字节偏移量和字节长度,byteOffset=0 表示视图从缓冲起点开始,byteLength=8 限制视图为前 8 个字节
const firstHalfDataView = new DataView(buf, 0, 8)
console.log(firstHalfDataView.byteOffset) // 0
console.log(firstHalfDataView.byteLength) // 8
console.log(firstHalfDataView.buffer === buf) // true

// 如果不指定,则 DataView 会使用剩余的缓冲,byteOffset=8 表示视图从缓冲的第 9 个字节开始,byteLength 未指定,默认为剩余缓冲
const secondHalfDataView = new DataView(buf, 8)
console.log(secondHalfDataView.byteOffset) // 8
console.log(secondHalfDataView.byteLength) // 8
console.log(secondHalfDataView.buffer === buf) // true

要通过 DataView 读取缓冲,还需要几个组件:

  • 首先是要读或写的字节偏移量。可以看成 DataView 中的某种“地址”
  • DataView 应该使用 ElementType 来实现 JavaScriptNumber 类型到缓冲内二进制格式的转换
  • 最后是内存中值的字节序。默认为大端字节序

ElementType

DataView 对存储在缓冲内的数据类型没有预设。它暴露的 API 强制开发者在读、写时指定一个 ElementType,然后 DataView 就会忠实地为读、写而完成相应的转换。ECMAScript 6 支持 8 种不同的 ElementType

ElementType字节说 明值的范围
Int818 位有符号整数-128~127
Uint818 位无符号整数0~255
Int16216 位有符号整数-32 768~32 767
Uint16216 位无符号整数0~65 535
Int32432 位有符号整数-2 147 483 648~2 147 483 647
Uint32432 位无符号整数0~4 294 967 295
Float32432 位 IEEE-754 浮点数-3.4e+38~+3.4e+38
Float64864 位 IEEE-754 浮点数-1.7e+308~+1.7e+308

DataView 为上表中的每种类型都暴露 getset 方法,这些方法使用 byteOffset(字节偏移量)定位要读取或写入值的位置。类型是可以互换使用的,如下例所示:

javascript
// 在内存中分配两个字节并声明一个 DataView
const buf = new ArrayBuffer(2)
const view = new DataView(buf)

// 说明整个缓冲确实所有二进制位都是 0
// 检查第一个和第二个字符
console.log(view.getInt8(0)) // 0
console.log(view.getInt8(1)) // 0

// 检查整个缓冲
console.log(view.getInt16(0)) // 0

// 将整个缓冲都设置为 1,255 的二进制表示是 11111111(2^8 - 1)
view.setUint8(0, 255)

// DataView 会自动将数据转换为特定的 ElementType,255 的十六进制表示是 0xFF
view.setUint8(1, 0xff)

// 现在,缓冲里都是 1 了
// 如果把它当成二补数的有符号整数,则应该是-1
console.log(view.getInt16(0)) // -1

字节序

前面例子中的缓冲有意回避了字节序的问题。字节序 指的是计算系统维护的一种字节顺序的约定

DataView 只支持两种约定:大端字节序和小端字节序

  • 大端字节序也称为 网络字节序,意思是最高有效位保存在第一个字节,而最低有效位保存在最后一个字节
  • 小端字节序正好相反,即最低有效位保存在第一个字节,最高有效位保存在最后一个字节

JavaScript 运行时所在系统的原生字节序决定了如何读取或写入字节,但 DataView 并不遵守这个约定。对一段内存而言,DataView 是一个中立接口,它会遵循指定的字节序。DataView 的所有 API 方法都以大端字节序作为默认值,但接收一个可选的布尔值参数,设置为 true 即可启用小端字节序

javascript
// 在内存中分配两个字节并声明一个 DataView
const buf = new ArrayBuffer(2)
const view = new DataView(buf)

// 填充缓冲,让第一位和最后一位都是 1
view.setUint8(0, 0x80)
view.setUint8(1, 0x01)

// 缓冲内容(为方便阅读,人为加了空格)
// 0x8 0x0 0x0 0x1
// 1000 0000 0000 0001
// 按大端字节序读取 Uint16

  // ... 中间省略 ...

view.setUint16(0, 0x0002, true)
// 缓冲内容(为方便阅读,人为加了空格)
// 0x0 0x2 0x0 0x0
// 0000 0010 0000 0000
console.log(view.getUint8(0)) // 2
console.log(view.getUint8(1)) // 0

边界情形

DataView 完成读、写操作的前提是必须有充足的缓冲区,否则就会抛出 RangeError

javascript
const buf = new ArrayBuffer(6)
const view = new DataView(buf)

// 尝试读取部分超出缓冲范围的值
view.getInt32(4) // RangeError

// 尝试读取超出缓冲范围的值
view.getInt32(8) // RangeError

// 尝试读取超出缓冲范围的值
view.getInt32(-1) // RangeError

// 尝试写入超出缓冲范围的值
view.setInt32(4, 123) // RangeError

DataView 在写入缓冲里会尽最大努力把一个值转换为适当的类型,后备为 0。如果无法转换,则抛出错误:

javascript
const buf = new ArrayBuffer(1)
const view = new DataView(buf)
view.setInt8(0, 1.5)
console.log(view.getInt8(0)) // 1

view.setInt8(0, [4])
console.log(view.getInt8(0)) // 4

view.setInt8(0, "f")
console.log(view.getInt8(0)) // 0
view.setInt8(0, Symbol())
// TypeError

定型数组概述

定型数组是另一种形式的 ArrayBuffer 视图。虽然概念上与 DataView 接近,但定型数组的区别在于,它特定于一种 ElementType 且遵循系统原生的字节序。相应地,定型数组提供适用面更广的 API 和更高的性能

类型列表

JavaScript 提供了以下 10 种定型数组类型,每种类型对应不同的数值范围和用途:

类型字节说明值的范围用途
Int8Array18 位有符号整数-128~127音频处理、图像处理
Uint8Array18 位无符号整数0~255图像数据、二进制协议
Uint8ClampedArray18 位无符号整数(夹板)0~255Canvas 图像数据
Int16Array216 位有符号整数-32,768~32,767音频采样、短整数数据
Uint16Array216 位无符号整数0~65,535Unicode 字符、网络协议
Int32Array432 位有符号整数-2,147,483,648~2,147,483,647大整数计算、索引
Uint32Array432 位无符号整数0~4,294,967,295大整数、颜色值
Float16Array216 位 IEEE-754 半精度浮点数±65504深度学习、GPU 计算、HDR(ES2025)
Float32Array432 位 IEEE-754 浮点数-3.4e+38~+3.4e+38WebGL、3D 图形、科学计算
Float64Array864 位 IEEE-754 浮点数-1.7e+308~+1.7e+308高精度计算、数学运算

创建定型数组

创建定型数组的方式包括读取已有的缓冲、使用自有缓冲、填充可迭代结构,以及填充基于任意类型的定型数组。另外通过<ElementType>.from()<ElementType>.of() 也可以创建定型数组:

javascript
const buf = new ArrayBuffer(12) // 创建一个 12 字节的缓冲
const ints = new Int32Array(buf) // 创建一个引用该缓冲的 Int32Array
// 这个定型数组知道自己的每个元素需要 4 字节,因此长度为 3
console.log(ints.length) // 3

const ints2 = new Int32Array(6) // 创建一个长度为 6 的 Int32Array
// 每个数值使用 4 字节,因此 ArrayBuffer 是 24 字节
console.log(ints2.length) // 6
// 类似 DataView,定型数组也有一个指向关联缓冲的引用
console.log(ints2.buffer.byteLength) // 24

// 创建一个包含 [2, 4, 6, 8]的 Int32Array

  // ... 中间省略 ...


// 基于传入的参数创建一个 Float32Array
const floats = Float32Array.of(3.14, 2.718, 1.618)
console.log(floats.length) // 3
console.log(floats.buffer.byteLength) // 12
console.log(floats[2]) // 1.6180000305175781

定型数组的构造函数和实例都有 BYTES_PER_ELEMENT 属性,返回该类型数组中每个元素的大小:

javascript
console.log(Int16Array.BYTES_PER_ELEMENT) // 2
console.log(Int32Array.BYTES_PER_ELEMENT) // 4

const ints = new Int32Array(1),
  floats = new Float64Array(1)

console.log(ints.BYTES_PER_ELEMENT) // 4
console.log(floats.BYTES_PER_ELEMENT) // 8

如果定型数组没有用任何值初始化,则其关联的缓冲会以 0 填充:

javascript
const ints = new Int32Array(4)

console.log(ints[0]) // 0
console.log(ints[1]) // 0
console.log(ints[2]) // 0
console.log(ints[3]) // 0

定型数组行为

从很多方面看,定型数组与普通数组都很相似。定型数组支持如下操作符、方法和属性:

  • []
  • fill()copyWithin()
  • keys()values()entries()
  • every()some()
  • filter()
  • find()findIndex()
  • forEach()
  • indexOf()lastIndexOf()
  • join()
  • length
  • map()
  • reduce()reduceRight()
  • slice()
  • sort()reverse()
  • toLocaleString()toString()

其中,返回新数组的方法也会返回包含同样元素类型(element type)的新定型数组:

javascript
const ints = new Int16Array([1, 2, 3])
const doubleints = ints.map((x) => 2 * x)
console.log(doubleints instanceof Int16Array) // true

定型数组有 Symbol.iterator 符号属性,因此可以通过 for..of 循环和扩展操作符来操作:

javascript
const ints = new Int16Array([1, 2, 3])
for (const int of ints) {
  console.log(int)
}

// 1、2、3
console.log(Math.max(...ints)) // 3

合并、复制和修改定型数组

定型数组同样使用数组缓冲来存储数据,而数组缓冲无法调整大小。因此下列方法不适用于定型数组:

  • concat()
  • pop()
  • push()
  • shift()
  • splice()
  • unshift()

但定型数组也提供两个新方法,可以快速向外或向内复制数据:set()subarray()

set

set 从提供的数组或定型数组中把值复制到当前定型数组中指定的索引位置:

javascript
// 创建长度为 8 的 int16 数组
const container = new Int16Array(8)

// 把定型数组复制为前 4 个值,偏移量默认为索引 0
container.set(Int8Array.of(1, 2, 3, 4))
console.log(container) // [1,2,3,4,0,0,0,0]

// 把普通数组复制为后 4 个值,偏移量 4 表示从索引 4 开始插入
container.set([5, 6, 7, 8], 4)
console.log(container) // [1,2,3,4,5,6,7,8]

// 溢出会抛出错误
container.set([5, 6, 7, 8], 7)
// RangeError

subarray

subarray 执行与 set 相反的操作,它会基于从原始定型数组中复制的值返回一个新定型数组。复制值时的开始索引和结束索引是可选的:

javascript
const source = Int16Array.of(2, 4, 6, 8)

// 把整个数组复制为一个同类型的新数组
const fullCopy = source.subarray()
console.log(fullCopy) // [2, 4, 6, 8]

// 从索引 2 开始复制数组
const halfCopy = source.subarray(2)
console.log(halfCopy) // [6, 8]

// 从索引 1 开始复制到索引 3
const partialCopy = source.subarray(1, 3)
console.log(partialCopy) // [4, 6]

定型数组没有原生的拼接能力,但使用定型数组 API 提供的很多工具可以手动构建:

javascript
// 第一个参数是应该返回的数组类型
// 其余参数是应该拼接在一起的定型数组
function typedArrayConcat(typedArrayConstructor, ...typedArrays) {
  // 计算所有数组中包含的元素总数
  const numElements = typedArrays.reduce((x, y) => (x.length || x) + y.length)
  // 按照提供的类型创建一个数组,为所有元素留出空间
  const resultArray = new typedArrayConstructor(numElements)
  // 依次转移数组
  let currentOffset = 0
  typedArrays.map((x) => {
    resultArray.set(x, currentOffset)
    currentOffset += x.length
  })
  return resultArray
}

const concatArray = typedArrayConcat(Int32Array, Int8Array.of(1, 2, 3), Int16Array.of(4, 5, 6), Float32Array.of(7, 8, 9))
console.log(concatArray) // [1, 2, 3, 4, 5, 6, 7, 8, 9]
console.log(concatArray instanceof Int32Array) // true

下溢和上溢

定型数组中值的下溢和上溢不会影响到其他索引,但仍然需要考虑数组的元素应该是什么类型。定型数组对于可以存储的每个索引只接受一个相关位,而不考虑它们对实际数值的影响。以下代码演示了如何处理下溢和上溢:

javascript
// 长度为 2 的有符号整数数组
// 每个索引保存一个二补数形式的有符号整数
// 范围是-128(-1 * 2^7)~127(2^7 - 1)
const ints = new Int8Array(2)

// 长度为 2 的无符号整数数组
// 每个索引保存一个无符号整数
// 范围是 0~255(2^7 - 1)
const unsignedInts = new Uint8Array(2)

// 上溢的位不会影响相邻索引
// 索引只取最低有效位上的 8 位

  // ... 中间省略 ...

console.log(ints) // [0, -128]

// 下溢自动变成二补数形式
// 0xFF 是无符号整数的 255,是二补数形式的-1
ints[1] = 255 // 0xFF
console.log(ints) // [0, -1]

除了 8 种元素类型,还有一种“夹板”数组类型:Uint8ClampedArray,不允许任何方向溢出。超出最大值 255 的值会被向下舍入为 255,而小于最小值 0 的值会被向上舍入为 0

javascript
const clampedInts = new Uint8ClampedArray([-1, 0, 255, 256])
console.log(clampedInts) // [0, 0, 255, 255]

按照 JavaScript 之父 Brendan Eich 的说法:"Uint8ClampedArray 完全是 HTML5canvas 元素的历史留存。除非真的做跟 canvas 相关的开发,否则不要使用它。"

定型数组与普通数组的区别

定型数组和普通数组在多个方面存在重要区别:

1. 内存分配

  • 普通数组:可以动态调整大小,元素可以是任意类型
  • 定型数组:固定大小,元素必须是特定数值类型
javascript
// 普通数组
const normalArray = [1, 2, 3]
normalArray.push(4) // ✅ 可以动态添加
normalArray[0] = "hello" // ✅ 可以存储任意类型

// 定型数组
const typedArray = new Int32Array([1, 2, 3])
// typedArray.push(4) // ❌ TypeError: typedArray.push is not a function
typedArray[0] = 4.7 // ✅ 会转换为整数 4
// typedArray[0] = "hello" // ❌ 会转换为 NaN 或 0

2. 性能

定型数组在数值计算方面性能显著优于普通数组:

javascript
// 性能测试示例
const size = 1000000

// 普通数组
const normalArray = new Array(size).fill(0).map(() => Math.random())
console.time("普通数组")
const normalSum = normalArray.reduce((a, b) => a + b, 0)
console.timeEnd("普通数组")

// 定型数组
const typedArray = new Float32Array(size).map(() => Math.random())
console.time("定型数组")
const typedSum = typedArray.reduce((a, b) => a + b, 0)
console.timeEnd("定型数组")
// 定型数组通常快 2-5 倍

3. 内存占用

定型数组的内存占用是固定的,可预测的:

javascript
const normalArray = [1, 2, 3, 4]
const typedArray = new Int32Array([1, 2, 3, 4])

// 定型数组:4 个元素 × 4 字节 = 16 字节
console.log(typedArray.buffer.byteLength) // 16

// 普通数组:每个元素都是对象引用,内存占用更大且不固定

4. 方法支持

定型数组不支持会改变数组大小的方法:

javascript
const typedArray = new Int32Array([1, 2, 3])

// ❌ 不支持的方法
// typedArray.push(4)
// typedArray.pop()
// typedArray.shift()
// typedArray.unshift(0)
// typedArray.splice(1, 1)
// typedArray.concat([4, 5])

// ✅ 支持的方法
typedArray.slice(1) // 返回新数组
typedArray.map((x) => x * 2) // 返回新数组
typedArray.filter((x) => x > 1) // 返回新数组

5. 类型转换

定型数组会自动进行类型转换和溢出处理:

javascript
const int8Array = new Int8Array([128, -1, 256])
console.log(int8Array) // Int8Array(3) [ -128, -1, 0 ]
// 128 溢出变成 -128,256 溢出变成 0

const uint8Array = new Uint8Array([-1, 256])
console.log(uint8Array) // Uint8Array(2) [ 255, 0 ]
// -1 转换为 255,256 溢出变成 0

实际应用场景

1. WebGL 图形渲染

定型数组是 WebGL 中传递顶点数据、颜色数据等的标准方式:

javascript
// 创建顶点位置数据
const vertices = new Float32Array([
  -1.0,
  -1.0,
  0.0, // 顶点 1
  1.0,
  -1.0,
  0.0, // 顶点 2
  0.0,
  1.0,
  0.0 // 顶点 3
])

  // ... 中间省略 ...

  255,
  255 // 蓝色
])

// 传递给 WebGL
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW)

2. Canvas 图像处理

处理 Canvas 像素数据时使用 Uint8ClampedArray

javascript
const canvas = document.createElement("canvas")
const ctx = canvas.getContext("2d")
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)

// imageData.data 是 Uint8ClampedArray
const pixels = imageData.data

// 处理像素数据(例如:灰度化)
for (let i = 0; i < pixels.length; i += 4) {
  const gray = (pixels[i] + pixels[i + 1] + pixels[i + 2]) / 3
  pixels[i] = gray // R
  pixels[i + 1] = gray // G
  pixels[i + 2] = gray // B
  // pixels[i + 3] 是 Alpha 通道
}

ctx.putImageData(imageData, 0, 0)

3. Web Audio API

处理音频数据时使用定型数组:

javascript
const audioContext = new AudioContext()
const bufferSize = 4096
const audioBuffer = audioContext.createBuffer(1, bufferSize, audioContext.sampleRate)

// 获取音频数据(Float32Array)
const channelData = audioBuffer.getChannelData(0)

// 生成正弦波
for (let i = 0; i < bufferSize; i++) {
  channelData[i] = Math.sin((2 * Math.PI * 440 * i) / audioContext.sampleRate)
}

4. 文件处理

读取和处理二进制文件:

javascript
async function readBinaryFile(file) {
  const arrayBuffer = await file.arrayBuffer()
  const uint8Array = new Uint8Array(arrayBuffer)

  // 处理二进制数据
  // 例如:解析文件头、提取特定数据等
  const header = uint8Array.slice(0, 4)
  console.log("File header:", Array.from(header))

  return uint8Array
}

5. 网络协议处理

处理网络协议中的二进制数据:

javascript
function parseNetworkPacket(buffer) {
  const view = new DataView(buffer)

  // 读取协议头(假设:前 4 字节是长度,接下来 2 字节是类型)
  const length = view.getUint32(0, true) // 小端字节序
  const type = view.getUint16(4, true)
  const payload = new Uint8Array(buffer, 6, length - 6)

  return { length, type, payload }
}

6. 数据压缩和加密

处理需要精确字节控制的数据:

javascript
// 简单的数据编码示例
function encodeData(data) {
  const buffer = new ArrayBuffer(data.length * 4)
  const view = new DataView(buffer)

  data.forEach((value, index) => {
    view.setFloat32(index * 4, value, true) // 小端字节序
  })

  return new Uint8Array(buffer)
}

function decodeData(uint8Array) {
  const view = new DataView(uint8Array.buffer)
  const result = []

  for (let i = 0; i < uint8Array.length; i += 4) {
    result.push(view.getFloat32(i, true))
  }

  return result
}

性能优化建议

1. 选择合适的类型

根据数据范围选择最小合适的类型:

javascript
// ❌ 浪费内存
const largeArray = new Int32Array(1000000) // 4MB

// ✅ 节省内存
const smallArray = new Int16Array(1000000) // 2MB(如果值在范围内)

2. 避免频繁创建

重用定型数组而不是频繁创建新实例:

javascript
// ❌ 性能差
function processData(data) {
  return new Float32Array(data).map((x) => x * 2)
}

// ✅ 性能好
const tempArray = new Float32Array(1000)
function processData(data, output = tempArray) {
  for (let i = 0; i < data.length; i++) {
    output[i] = data[i] * 2
  }
  return output
}

3. 使用共享内存

多个视图可以共享同一个 ArrayBuffer:

javascript
const buffer = new ArrayBuffer(16)

// 共享同一块内存
const int8View = new Int8Array(buffer)
const int16View = new Int16Array(buffer)

int8View[0] = 1
console.log(int16View[0]) // 值会反映在共享视图中

4. 批量操作

使用 set() 方法批量复制数据:

javascript
// ❌ 逐个赋值
for (let i = 0; i < source.length; i++) {
  target[i] = source[i]
}

// ✅ 批量复制
target.set(source)

常见错误和注意事项

1. 数组大小不可变

定型数组创建后大小固定,不能动态调整:

javascript
const arr = new Int32Array(5)
// arr.push(6) // ❌ TypeError
// arr.length = 10 // ❌ 无效,长度不会改变

// ✅ 需要创建新数组
const newArr = new Int32Array([...arr, 6])

2. 类型溢出

注意数值溢出问题,特别是无符号类型:

javascript
const uint8 = new Uint8Array([255])
uint8[0] += 1
console.log(uint8[0]) // 0,不是 256

// ✅ 检查溢出
if (uint8[0] < 255) {
  uint8[0] += 1
}

3. 字节序问题

在不同系统间传输数据时要注意字节序:

javascript
// ❌ 可能在不同系统间出错
const view = new DataView(buffer)
const value = view.getInt32(0) // 默认大端字节序

// ✅ 明确指定字节序
const value = view.getInt32(0, true) // 小端字节序

4. 类型转换陷阱

定型数组会自动转换类型,可能导致意外结果:

javascript
const int8 = new Int8Array([1, 2, 3])
int8[0] = 1.7 // 转换为 1
int8[0] = "2" // 转换为 2
int8[0] = NaN // 转换为 0
int8[0] = Infinity // 转换为 0 或 -1(取决于类型)

// ✅ 显式转换
int8[0] = Math.floor(1.7)

5. 共享内存的副作用

多个视图共享 ArrayBuffer 时,修改会相互影响:

javascript
const buffer = new ArrayBuffer(4)
const view1 = new Int32Array(buffer)
const view2 = new Int32Array(buffer)

view1[0] = 42
console.log(view2[0]) // 42,共享同一块内存

// ✅ 需要独立数据时创建新缓冲
const view3 = new Int32Array(buffer.slice()) // 复制缓冲

6. 与普通数组的互操作

定型数组可以转换为普通数组,但会丢失性能优势:

javascript
const typedArray = new Int32Array([1, 2, 3])
const normalArray = Array.from(typedArray) // 或 [...typedArray]

// ✅ 尽量保持为定型数组进行操作
const doubled = typedArray.map((x) => x * 2) // 返回新的定型数组