精读《JavaScript 高级程序设计 第4版》第6章 集合引用类型(二)定型数组
6.3 定型数组
在定型数组出现之前,JavaScript只有一种数值类型Number,它是基于IEEE 754标准的64位双精度浮点数。这对于处理图形、音频、视频等需要大量、密集、特定格式(如8位无符号整数、32位浮点数)的二进制数据来说,存在严重问题:
- 精度不匹配:图形API(如WebGL)需要的是32位浮点数,而JS的
Number是64位的,直接使用效率低下且格式不符。 - 性能瓶颈:从JS中动态转换和传递大量
Number对象到原生库,性能开销巨大。 - 缺乏字节操作能力:无法直接读取和操作构成文件、图像或网络数据包的原始字节。
定型数组的出现,就是为了让JavaScript能够像C、Java等系统级语言一样,高效地操作和传递一块原始的二进制内存。
6.3.1 ArrayBuffer - 原始的二进制数据“仓库”
ArrayBuffer是所有定型数组及视图引用的基本单位。它代表了一段原始的、不分类型的二进制数据内存块。
- 你无法直接操作
ArrayBuffer本身。它只是一个“仓库”,你不知道里面装的是什么,也不知道如何存取。 - 它的核心属性是
byteLength,表示缓冲区的长度(以字节为单位)。 ArrayBuffer()是一个普通的IavaScript构造函数,用于在内存中分配特定数量的字节空间。ArrayBuffer一经创建就不能再调整大小,但可以通过slice()复制其部分或全部到一个新实例中。ArrayBuffer在分配失败时会抛出错误,且分配内存不能大于Number.MAX_SAFE_INTEGER(253 - 1)字节。- 声明
ArrayBuffer会将所有二进制位初始化为0,且分配的堆内存可以被垃圾回收,不用手动释放。
// 创建一个16字节(128位)的缓冲区
const buffer = new ArrayBuffer(16);
const buffer2 = buffer.slice(4, 12);
console.log(buffer.byteLength); // 16
console.log(buffer2.byteLength);//8
6.3.2 DataView - 灵活的“万能钥匙”
为了读取和写入ArrayBuffer,你需要一个“视图”。DataView是第一种视图,它提供了最灵活的API,允许你从缓冲区的任意偏移量开始,以不同的数值类型来解读数据。该视图专为文件I/O和网络I/O设计,对缓存内容没有任何预设,也无法迭代。
- 适用于:数据格式不固定、混合数据类型(如在文件头中,前4个字节是整数,后8个字节是双精度浮点数)。
const buf = new ArrayBuffer(16);
//默认使用整个ArrayBuffer
const view = new DataView(buf);
console.log(view.byteOffset); //0
console.log(view.byteLength); //16
console.log(view.buffer === buf); //true
//DataView接收一个可选的字节偏移量和字节长度
const view2 = new DataView(buf,0,8);
console.log(view2.byteOffset); //0
console.log(view2.byteLength); //8
console.log(view2.buffer === buf); //true
ElementType
DataView暴露的API强制开发者在读、写时指定一个ElementType,然后DataView就会忠实地为读、写完成相应的转换。ECMAScript6支持8种不同的ElementType:
|
ElementType |
字节 |
说明 |
值的范围 |
|
Int8 |
1 |
8位有符号整数 |
-128~127 |
|
Uint8 |
1 |
8位无符号整数 |
0~255 |
|
Int16 |
2 |
16位有符号整数 |
-32 768~32 767 |
|
Uint16 |
2 |
16位无符号整数 |
0~65 535 |
|
Int32 |
4 |
32位有符号整数 |
-2147483648~2147483647 |
|
Uint32 |
4 |
32位无符号整数 |
0~4294967295 |
|
Float32 |
4 |
32位IEEE-754浮点数 |
-3.4e+38~+3.4e+38 |
|
Float64 |
8 |
64位IEEE-754浮点数 |
-1.7e+308~+1.7e+308 |
DataView为上表的每种类型都暴露了get和set方法,类型可以互换使用。
字节序
字节序(Endianness)指数据在内存中的存储顺序。DataView允许你显式控制。
- 大端序:最高有效字节在前(网络字节序,也是人脑习惯的阅读顺序)。
- 小端序:最低有效字节在前(x86/x64架构CPU的默认顺序)。
边界情形
DataView完成读、写操作的前提是必须有足够的缓冲区,否则抛出RangeError。
// 基于上面创建的buffer创建一个DataView
const view = new DataView(buffer);
// 在第0个字节处,写入一个16位有符号整数(大端序)
view.setInt16(0, 32767, false); // 参数:偏移量,值,是否小端序(false为大端)
// 在第2个字节处,写入一个32位无符号整数(小端序)
view.setUint32(2, 0xFFFF, true);
// 从第0个字节开始,读取一个16位有符号整数(大端序)
console.log(view.getInt16(0, false)); // 32767
// 从第2个字节开始,读取一个32位无符号整数(小端序)
console.log(view.getUint32(2, true).toString(16)); // "ffff"
6.3.3 定型数组视图 - 高效的“专用工具”
除了灵活的DataView,ECMAScript还定义了一系列特定类型的视图,即定型数组,如Int8Array, Uint32Array, Float64Array等。目的为了提高与WebGL等原生库交换二进制数据的效率。
- 它们既是视图,也是数组。它们提供了对
ArrayBuffer特定类型的视图,同时拥有类似普通数组的API(如length,map,filter等,但行为有细微差别)。
创建方式多样:
// 1. 从已有的ArrayBuffer创建(最常见的用法)
const buffer = new ArrayBuffer(16);
const int32View = new Int32Array(buffer); // 将buffer视为32位整数数组
// 因为buffer是16字节,32位=4字节,所以这个视图可以容纳 16/4 = 4 个整数
console.log(int32View.length); // 4
// 2. 直接创建,JS引擎会在背后自动创建和分配ArrayBuffer
const floatArray = new Float64Array(8); // 创建一个能容纳8个64位浮点数的定型数组
console.log(floatArray.buffer.byteLength); // 8 * 8 = 64 字节
// 3. 从另一个定型数组或可迭代对象创建
const fromAnother = new Uint8Array([1, 2, 3, 4]);
高效操作:
一旦建立,你可以像操作数组一样操作它们,但底层是直接的二进制操作,速度极快。支持多种与普通数组一样的操作符、方法与属性,如:copyWithin()、filter()、join()、keys()、length、slice()、sort()等
const uint8 = new Uint8Array(4);
uint8[0] = 0x41; // 'A' 的ASCII码
uint8[1] = 0x42; // 'B'
uint8[2] = 0x43; // 'C'
uint8[3] = 0x00; // 空字符
// 它们可以用于TextDecoder/TextEncoder与字符串互转
const decoder = new TextDecoder();
console.log(decoder.decode(uint8)); // "ABC"
const encoder = new TextEncoder();
const encoded = encoder.encode("Hello");
console.log(encoded); // Uint8Array(5) [ 72, 101, 108, 108, 111 ]
合并、复制与修改定型数组:
定型数组同样使用数组缓冲来存储数据,而数组缓冲无法调整大小。因此很多方法无法用于定型数组,如:concat()、pop()、push()、shift()、splice()、unshift()
但是定型数组也提供了两个新方法,可以快速向外或向内复制数据:set()和subarray()。
set()
从提供的数组或定型数组中把值复制到当前定型数组中的指定索引位置:
const container = new Int16Array(8);
//偏移量默认为0,把定型数组复制为前4个值
container.set(Int8Array.of(1,2,3,4));
console.log(container);//Int16Array(8) [1, 2, 3, 4, 0, 0, 0, 0]
//偏移量为4,把定型数组复制为后4个值
container.set([5,6,7,8], 4);
console.log(container); //Int16Array(8) [1, 2, 3, 4, 5, 6, 7, 8]
//偏移量为7,超出定型数组的范围,会报错
container.set([5,6,7,8], 7);
console.log(container); //RangeError: offset is out of bounds
subarray()
与set()相反,会基于从原始定型数组中复制的值返回一个新的定型数组。复制时开始索引和结束索引可选。
const source = Int16Array.of(2,4,6,8);
const fullCopy = source.subarray();
console.log(fullCopy);//[2,4,6,8]
const halfCopy = source.subarray(2);
console.log(halfCopy);//[6,8]
const partialCopy = source.subarray(1,3);
console.log(partialCopy);//[4,6]
6.3.4 现代JavaScript的演进与增强
1. 大整形 (BigInt) 定型数组
ES2020引入了BigInt,用于表示大于2^53 - 1的整数。相应地,也新增了BigInt64Array和BigUint64Array来处理64位整形数据,这在与WebAssembly等交互时至关重要。
const bigArray = new BigInt64Array(2);
bigArray[0] = 2n ** 63n - 1n; // 最大有符号64位整数
bigArray[1] = -1n;
console.log(bigArray[0]); // 9223372036854775807n
2. 流操作与分块处理 (Streams API)
现代Web应用中,数据常常是流式的。ReadableStream和WritableStream可以直接处理Uint8Array等定型数组,用于高效的文件上传下载、视频流处理等。
// 示例:从响应中流式读取数据
const response = await fetch('some-large-file.bin');
const reader = response.body.getReader();
while (true) {
const { done, value } = await reader.read(); // value 是一个 Uint8Array chunk
if (done) break;
// 处理这一块二进制数据
processChunk(value);
}
3. 与 WebAssembly 的深度集成
WebAssembly(Wasm)的内存(WebAssembly.Memory)本质上就是一个可增长的ArrayBuffer。JavaScript与Wasm之间通过共享这个ArrayBuffer来进行高速数据交换。
// 假设 wasmInstance 是一个已实例化的WebAssembly模块
const wasmMemory = wasmInstance.exports.memory;
const wasmMemoryBuffer = wasmMemory.buffer; // 这是一个ArrayBuffer
// 创建一个视图来直接操作Wasm的内存
const heap = new Uint8Array(wasmMemoryBuffer);
heap[0] = 42; // 直接在Wasm内存中写入数据
4. Atomics 与 SharedArrayBuffer 用于多线程
为了在Web Worker之间实现真正的共享内存和高效并行计算,引入了SharedArrayBuffer(共享的缓冲区)和Atomics(原子操作)对象。
SharedArrayBuffer:与ArrayBuffer类似,但可以被多个线程共享。Atomics:提供了一套不可中断的(原子的)操作,如add,compareExchange等,用于安全地在共享内存上进行读写,避免竞争条件。
注意:由于安全原因(如Spectre漏洞),SharedArrayBuffer的使用受到严格限制,通常要求页面设置特定的HTTP头部(如Cross-Origin-Opener-Policy和Cross-Origin-Embedder-Policy)。
6.3.5 定型数组总结与最佳实践
- 理解架构:始终记住
ArrayBuffer(数据块) -> 视图(DataView或定型数组,操作接口)这个模型。 - 选择合适的视图:
-
- 需要处理混合数据类型或复杂结构? -> 使用
DataView。 - 处理单一类型的大量数据,追求最高性能? -> 使用对应的定型数组(如
Float32Array用于WebGL,Uint8Array用于文件/网络数据)。
- 需要处理混合数据类型或复杂结构? -> 使用
- 拥抱现代API:在处理文本时,使用
TextEncoder/TextDecoder与Uint8Array互转。处理流数据时,结合Streams API。 - 注意平台差异:在使用
DataView时,要留意字节序问题。大多数情况下,现代CPU是小端序,但网络传输通常使用大端序。 - 安全与性能:
SharedArrayBuffer和Atomics是强大的工具,但使用门槛高,且主要用于高级并行计算场景。
更多推荐



所有评论(0)