JavaScript - TypedArray一文搞懂 JavaScript 中的类型化数组,要认识什么是类型化数组、为
一文搞懂 JavaScript 中的类型化数组
文章大纲:
- 什么是
TypedArray
- 为什么会有类型化数组
- 常用的类型化数组类型
- 类型化数组的深入研究
什么是 TypedArray
知其然知其所以然,我们在深入了解 类型化数组 (TypedArray
) 之前,先来认识一下: 为什么我们需要类型化数组:
为什么会有类型化数组
在 JavaScript 当中,开发者并不能直接对二进制内存进行操作,这也成为了 JavaScript 对 媒体内容 (图像、视频、音频) 处理的能力相对于其他语言来说,没有那么强。
在 WebGL (Web Graphics Library
) 蓬勃发展的时候,就出现了一个问题:
- JavaScript 在面对
2D
、3D
图像的处理时,需要通过给定的数组来计算大量的数值和图像数据,它的性能表现非常不佳
那么这个时候,聪明你或许会问了,明明都是一样对数组进行操作,为什么 WebGL 在渲染的时候会性能不佳呢?
那我们就来分析一下这个问题吧:
为什么会性能不佳
WebGL渲染时性能不佳的原因主要有以下几点:
-
数据类型转换的开销:
- JavaScript 的数组类型是 动态定义 的:JavaScript 数组的元素类型是动态的,这意味着每次访问数组元素时,JavaScript 引擎都需要进行类型检查和转换
- WebGL 的数据类型 十分严格:WebGL渲染时需要的是固定的类型数据,比如 32位浮点数。JavaScript的动态类型数组就导致了 数据类型与WebGL需求类型的不符合,数据转换的开销非常大。
-
JavaScript 的内存管理问题
- JavaScript 会定期对内存中的数据进行扫描,同时回收不需要的数据。如果你学过 JavaScript 的垃圾回收机制,那么你会知道,这个行为会中断运行,也会一定程度上的导致运行性能下降。
- JavaScript 的内存碎片问题:JavaScript分配的内存并不一定是连续的,JavaScript 也没有提供类似于 C 语言的
malloc
方法让开发者来分配内存,而是将数据碎片的存入空余内存中,这也导致了 JavaScript 在分配
、读写
内存上的效率问题。
-
数据传递时的数据拷贝
- 在 JavaScript 向 WebGL 传递数据的时候,会将数据拷贝一份并保存到 WebGL 的缓冲区,数据拷贝也是一项非常大的开销。
-
早期 JavaScript 引擎的优化问题
上述 4 点也就是 JavaScript 在处理媒体数据时性能不佳的主要原因。而这些问题,促进了 TypedArray
的诞生。
TypedArray
在了解了为什么会有 TypedArray
后,我们来看看它的特性吧:
- 固定的数据类型:
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位浮点数数组 | 存储浮点数,如坐标、颜色值 | 低精度浮点 |
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 这个范围,那么结果会是:
- 如果你提供了一个负数
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
中的数据,必须通过视图 (TypedArray
或DataView
) 来进行操作
示例
// 创建一个长度为 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 位的浮点数。
使用案例
- 在 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 在处理二进制数据方面的性能。
注意点:
- 类型数组的元素类型是固定的,一旦创建就不能改变。
- 类型数组的长度是固定的,不能动态增加或减少。
- 直接操作底层内存,如果使用不当可能会导致报错或者程序崩溃。
Biya Biya~ ♥ By Sharco Saviya
转载自:https://juejin.cn/post/7423287213129744396