• 作者:老汪软件技巧
  • 发表时间:2024-10-10 11:04
  • 浏览量:

一文搞懂 JavaScript 中的类型化数组

文章大纲:

常用的类型化数组类型类型化数组的深入研究什么是 TypedArray

知其然知其所以然,我们在深入了解 类型化数组 (TypedArray) 之前,先来认识一下: 为什么我们需要类型化数组:

为什么会有类型化数组

在 JavaScript 当中,开发者并不能直接对二进制内存进行操作,这也成为了 JavaScript 对 媒体内容 (图像、视频、音频) 处理的能力相对于其他语言来说,没有那么强。

在 WebGL (Web Graphics Library) 蓬勃发展的时候,就出现了一个问题:

那么这个时候,聪明你或许会问了,明明都是一样对数组进行操作,为什么 WebGL 在渲染的时候会性能不佳呢?

那我们就来分析一下这个问题吧:

为什么会性能不佳

WebGL渲染时性能不佳的原因主要有以下几点:

数据类型转换的开销:

JavaScript 的内存管理问题

数据传递时的数据拷贝

早期 JavaScript 引擎的优化问题

上述 4 点也就是 JavaScript 在处理媒体数据时性能不佳的主要原因。而这些问题,促进了 TypedArray 的诞生。

TypedArray

在了解了为什么会有 TypedArray 后,我们来看看它的特性吧:

总结来说,TypedArray 的出现,使得 JavaScript 可以更直接、高效地操作二进制数据,也显著提升了 WebGL 等图形处理应用的性能。

MDN:

JavaScript 的类型化数组中的每一个元素都是以某种格式表示的原始二进制值,JavaScript 支持从 8 位整数到 64 位浮点数的多种二进制格式。

类型化数组拥有许多与数组相同的方法,语义也相似。但是除了元素类型不同外,还有一些地方并不一样,比如:通过方法 Array.is(typedArray) 来对类型化数组进行判断会返回 false,并且类型化数组并不是支持所有数组的方法,比如:pop 和 push

TypedArray 构造器最终会产生一个可迭代对象,并且提供了 slice(start, end)切片和set(sameTypeArr)复制 实例方法。同时,可以通过 Array.from() 方法,将类型化数组转换为普通数组:

const int_8 = new Int8Array([1, 2, 3, 4]);
const slice = int_8.slice(1, 3);
console.log(slice); // Int8Array [ 2, 3 ]
const int_8_2 = new Int8Array(2);
int_8_2.set(slice);
console.log(int_8_2); // Int8Array [ 2, 3 ]
const normal_arr = Array.from(int_8_2);
console.log(normal_arr); // [ 2, 3 ]
console.log(normal_arr instanceof Array); // true

常用的类型化数组类型

下表列举了 JavaScript 中所有的类型化数组 (截至ES2025)

类型化数组描述用途范围

Int8Array

8位有符号整数数组

存储小整数,如字节数组、颜色值

-128 ~ 127

Uint8Array

8位无符号整数数组

存储字节数组、颜色值、索引

0 ~ 255

Uint8ClampedArray

8位无符号整数数组 (夹断)

存储颜色值

0 ~ 255 (超出会夹断)

Int16Array

16位有符号整数数组

存储较大范围的整数

-32768 ~ 32767

Uint16Array

16位无符号整数数组

存储较大范围的整数,如索引

0 ~ 65535

Int32Array

32位有符号整数数组

存储较大范围的整数

-2147483648 ~ 2147483647

Uint32Array

32位无符号整数数组

存储较大范围的整数,如索引

0 ~ 4294967295

Float32Array

32位浮点数数组

存储浮点数,如坐标、颜色值

数组类型_数组常用方法js_

低精度浮点

Float64Array

64位浮点数数组

存储高精度浮点数

高精度浮点

BigInt64Array

64位有符号 BigInt 数组

存储大型数据

-2^63 ~ 2^63 - 1

BigUInt64Array

64位无符号 BigInt 数组

存储大小数组

0 ~ 2^64 - 1

示例

我们简单介绍一下 Uint8Array 和 Uint8ClampedArray:

Uint8Array

const uint_8 = new Uint8Array();
// 和数组访问一样,我们通过方括号表示法来访问
uint_8[0] = 0; // Uint8Array的范围是 0 ~ 255
uint_8[1] = 255;
console.log(uint_8)

打印结果如下:

Uint8Array(2) [ 0, 255, /* ... */ ]

那么,如果说,我定义的数据超出了它的范围会怎么样?

const uint_8 = new Uint8Array();
// Uint8Array的范围是 0 ~ 255
uint_8[0] = -1; // 低于 0
uint_8[1] = 256; // 高于 255
console.log(uint_8)

我们来看一下打印结果:

Uint8Array(2) [ 255, 0, /* ... */ ]

是不是你和我一样,看到这个结果的时候不由得冒出三个问号?为什么会这样......

其实这是 Unit8Array 的夹断机制,如果提供的数超出了 0 ~ 255 这个范围,那么结果会是:

我们来计算一下 -1:

再来再来,我们再来计算一下 256:

Uint8ClampedArray

// 范围 0 ~ 255
const uint_8_c = new Uint8ClampedArray(2);
uint_8_c[0] = -1;
uint_8_c[1] = 256;
console.log(uint_8_c);

直接来看一下结果:

Uint8ClampedArray(2) [ 0, 255, /*...*/ ]

我们发现,这两个结果并不相同,这是因为它们夹断机制不同导致的:Uin8Array的夹断通过上述两个式子来进行计算最终结果,但是 Uint8ClampedArray 则是:

这便是两者夹断的不同。

类型化数组的深度研究ArrayBuffer

什么是 ArrayBuffer?

相信很多人在执行上述代码之后,会发现,答应出来的数据包含了一个 buffer 对象,这个 buffer 对象的原型指向了 ArrayBuffer,我们也可以直接通过 console.log(typedArr.buffer) 来打印这个 buffer 对象。

那么,我们来看看 什么是 ArrayBuffer:

ArrayBuffer 是 JavaScript 中用来表示通用原始二进制数据缓冲区的一个对象,它的特性是:

示例

// 创建一个长度为 8 字节的 ArrayBuffer
const buffer = new ArrayBuffer(8);
// 创建一个 Float32Array 视图
const float_32 = new Float32Array(buffer);
// 写入数据
float_32[0] = 1;
float_32[1] = 2;
float_32[2] = 3;
console.log(float_32);

我们创建了一个长度为 8个字节 的缓冲区,并且创建了一个类型化数组,把缓冲区作为参数传递给了这个构造函数,那么我们希望的结果是 Float32Array(3) [ 1, 2, 3 ],我们来看一下打印结果:

Float32Array(2) [ 1, 2 ]

我们发现,仅仅只有两位,内容是 0、1,这是为什么呢?其实很简单:

我们首先通过 ArrayBuffer 创建了一个 8 字节 的缓冲区,那么这个缓冲区的存储上限也就是 8 字节

我们来看一下通过它创建的缓冲区:

0000 0000
0000 0000
/* ... */
0000 0000

每一个 0000 0000 都能存储 1 字节 的数据,8 字节 就一共有 8 个。每个元素都单独存储在一个字节中,而经过 Float32Array 之后,每个元素占连续的 4 字节,每个元素可占 32 位,就像:

[Element0]:
0000 0000
0000 0000
0000 0000
0000 0000
[Element1]:
/* ... */

接着,我们向这个 Float32Array 的缓冲区中存储数据,来看一下缓冲区的变化:

/* 为了方便,就缩成一行 */
[Element0]:
00000000 00000000 00000000 00000001 --> 1
[Element1]:
00000000 00000000 00000000 00000010 --> 2

当我们存入第三个元素的时候,虽然代码继续运行了下去,但是事实上并没有将第三个元素保存到缓冲区当中,是因为没有多余的空间来存储第三个元素了,除非我们扩大缓冲区:

// 从原来的 ArrayBuffer(8) 变成了 ArrayBuffer(12)
const buffer = new ArrayBuffer(12);
const float_32 = new Float32Array(buffer);
float_32[0] = 0;
float_32[1] = 1;
float_32[2] = 2;
console.log(float_32);

我们扩大了 4字节 的缓冲区,便能再多存下一个 32 位的浮点数。

使用案例

const imageData = new Uint8ClampedArray(16 * 16 * 4);
// 填充红色
for (let i = 0; i < imageData.length; i += 4) {
imageData[i + 0] = 255; // Red
imageData[i + 1] = 0; // Green
imageData[i + 2] = 0; // Blue
imageData[i + 3] = 255; // Alpha
}
// 创建一个 Canvas 元素并获取其绘图上下文
const canvas = document.createElement("canvas");
canvas.width = 16;
canvas.height = 16;
document.body.appendChild(canvas);
const ctx = canvas.getContext("2d");
// 将图像数据设置到 Canvas
const imageDataObj = ctx.createImageData(16, 16);
imageDataObj.data.set(imageData);
ctx.putImageData(imageDataObj, 0, 0);

总结

类型数组是 JavaScript 处理二进制数据的重要工具,它提供了高效、直接的方式来操作底层内存。通过理解类型数组的特性和使用方法,可以显著提升 JavaScript 在处理二进制数据方面的性能。

注意点:

Biya Biya~ ♥ By Sharco Saviya