JavaScript - TypedArray

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

文章大纲:

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

什么是 TypedArray

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

为什么会有类型化数组

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

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

  • JavaScript 在面对 2D3D 图像的处理时,需要通过给定的数组来计算大量的数值和图像数据,它的性能表现非常不佳

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

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

为什么会性能不佳

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

  1. 数据类型转换的开销:

    • JavaScript 的数组类型是 动态定义:JavaScript 数组的元素类型是动态的,这意味着每次访问数组元素时,JavaScript 引擎都需要进行类型检查和转换
    • WebGL 的数据类型 十分严格:WebGL渲染时需要的是固定的类型数据,比如 32位浮点数。JavaScript的动态类型数组就导致了 数据类型与WebGL需求类型的不符合,数据转换的开销非常大。
  2. JavaScript 的内存管理问题

    • JavaScript 会定期对内存中的数据进行扫描,同时回收不需要的数据。如果你学过 JavaScript 的垃圾回收机制,那么你会知道,这个行为会中断运行,也会一定程度上的导致运行性能下降。
    • JavaScript 的内存碎片问题:JavaScript分配的内存并不一定是连续的,JavaScript 也没有提供类似于 C 语言的 malloc 方法让开发者来分配内存,而是将数据碎片的存入空余内存中,这也导致了 JavaScript 在 分配读写 内存上的效率问题。
  3. 数据传递时的数据拷贝

    • 在 JavaScript 向 WebGL 传递数据的时候,会将数据拷贝一份并保存到 WebGL 的缓冲区,数据拷贝也是一项非常大的开销。
  4. 早期 JavaScript 引擎的优化问题

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

TypedArray

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

  • 固定的数据类型TypedArray 不再像普通数组一样,可以随意的动态定义数据类型,而是固定了数组元素的类型
  • 连续的内存TypedArray 的元素在内存中变成了连续存储,提高了内存访问效率
  • 直接内存访问TypedArray 可以直接操作底层内存,减少数据拷贝的次数

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

MDN:

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

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

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)

类型化数组描述用途范围
Int8Array8位有符号整数数组存储小整数,如字节数组、颜色值-128 ~ 127
Uint8Array8位无符号整数数组存储字节数组、颜色值、索引0 ~ 255
Uint8ClampedArray8位无符号整数数组 (夹断)存储颜色值0 ~ 255 (超出会夹断)
Int16Array16位有符号整数数组存储较大范围的整数-32768 ~ 32767
Uint16Array16位无符号整数数组存储较大范围的整数,如索引0 ~ 65535
Int32Array32位有符号整数数组存储较大范围的整数-2147483648 ~ 2147483647
Uint32Array32位无符号整数数组存储较大范围的整数,如索引0 ~ 4294967295
Float32Array32位浮点数数组存储浮点数,如坐标、颜色值低精度浮点
Float64Array64位浮点数数组存储高精度浮点数高精度浮点
BigInt64Array64位有符号 BigInt 数组存储大型数据-2^63 ~ 2^63 - 1
BigUInt64Array64位无符号 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)

我们来看一下打印结果:

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

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

  • 如果你提供了一个负数 x,它的计算结果是:256 + x % 256
  • 如果你提供了一个正数 x,它的计算结果是:x % 256

我们来计算一下 -1

  • -1 % 256 的结果是:-1
  • 256 + (-1) 的结果是: 255

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

  • 256 % 256 那么结果就直接为 0
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 则是:

  • 如果数据 x 并且 x < 0,那么直接返回 0
  • 如果数据 x 并且 x > 255,那么直接返回 255

这便是两者夹断的不同。

类型化数组的深度研究

ArrayBuffer

什么是 ArrayBuffer?

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

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

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

  • 固定长度:一旦创建,ArrayBuffer 的大小就固定了,无法动态改变
  • 不能直接读写:你不能直接访问或修改 ArrayBuffer 中的单个字节
  • 视图:视图的作用是将缓冲区中的数据解释为特定的格式,要操作 ArrayBuffer 中的数据,必须通过视图 (TypedArrayDataView) 来进行操作
示例
// 创建一个长度为 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 ]

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

我们首先通过 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 位的浮点数。

使用案例

  • 在 canvas 上绘制一个 16 x 16 的红色图像
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 在处理二进制数据方面的性能。

注意点:

  • 类型数组的元素类型是固定的,一旦创建就不能改变。
  • 类型数组的长度是固定的,不能动态增加或减少。
  • 直接操作底层内存,如果使用不当可能会导致报错或者程序崩溃。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值