ES6重读及新感(Arraybuffer类重读)

本文详细介绍了JavaScript中的ArrayBuffer对象及其视图,包括ArrayBuffer的作用、API,以及如何通过视图(TypedArray和DataView)进行二进制数据操作。此外,还讨论了字节序、ArrayBuffer与字符串的转换、溢出规则,以及SharedArrayBuffer和Atomics对象在多线程中的应用。ArrayBuffer在Node.js中尤为重要,对于处理文件操作和网络数据解析非常关键。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

 

前言:

ArrayBuffer类在浏览器端并不会很常用,浏览器端使用该方法,大部分用于处理上传文件的操作(html5中新增了webgl,也会用来写一些喂webgl功能),因此很多前端同学会认为这个语法糖并没有太大的作用。实际来说,在浏览器端大部分需要二进制流操作的部分,都由浏览器端,帮我们解决了,最典型的列子即http请求。如果浏览器端不进行处理,那我们就需要频繁的操作二进制流进行http协议的解析和数据的解析。但ArrayBuffer对node是很常见的,由于node端有大量涉及文件操作的需求,因此掌握ArrayBuffer类还有有足够必要的

一、ArrayBuffer 对象

1、作用:

用来创建二进制数据片段,不能直接读写,只能通过“视图”(TypedArray、DataView)进行操作。对同一个ArrayBuffer对象生成的内存中,生成多个视图,当对视图进行修改时,内存对象中的数据也会改变。创建ArrayBuffer对象以及视图,计算机只会分配固定的内存空间,而不涉及硬盘空间的写入。

本来,在设计目的上,ArrayBuffer对象的各种TypedArray视图,是用来向网卡、声卡之类的本机设备传送数据,所以使用本机的字节序就可以了;而DataView视图的设计目的,是用来处理网络设备传来的数据,所以大端字节序或小端字节序是可以自行设定的。

2、API:

byteLength:创建的ArrayBuffer内存片段,获取buffer的长度(二进制),如果要分配的内存区域很大,有可能分配失败(因为没有那么多的连续空余内存),所以有必要检查是否分配成功。

if (buffer.byteLength === n) {
  // 检测成功
} else {
  // 检测失败
}

slice:该方法会拷贝生成新的ArrayBuffer对象,api其他操作同Array.prototype.slice。slice方法其实包含两步,第一步是先分配一段新内存,第二步是将原来那个ArrayBuffer对象拷贝过去

isView:用来表示参数是否为ArrayBuffer的视图实例。

const buffer = new ArrayBuffer(8);
ArrayBuffer.isView(buffer) // false

const v = new Int32Array(buffer);
ArrayBuffer.isView(v) // true

 

二、TypedArray视图

1、作用:

用来对已经创建的ArrayBuffer对象创建视图,视图中可以选择多种数据类型,方便操作各种情况的数据。只能创建一种数据类型的视图

TypedArray 数组的所有成员,都是同一种类型。

TypedArray 数组的成员是连续的,不会有空位。

TypedArray 数组成员的默认值为 0。比如,new Array(10)返回一个普通数组,里面没有任何成员,只是 10 个空位;new Uint8Array(10)返回一个 TypedArray 数组,里面 10 个成员都是 0。

TypedArray 数组只是一层视图,本身不储存数据,它的数据都储存在底层的ArrayBuffer对象之中,要获取底层对象必须使用buffer属性。

2、API:

constructor:

(1)、使用TypedArray视图时,创建的视图区域开始范围必须与建立视图所需要的位数相同,否则会报错。

(2)、视图还可以不通过ArrayBuffer对象,直接分配内存而生成。

(3)、TypedArray 数组的构造函数,可以接受另一个TypedArray实例作为参数。注意,此时生成的新数组,只是复制了参数数组的值,对应的底层内存是不一样的。新数组会开辟一段新的内存储存数据,不会在原数组的内存之上建立视图。

如下代码:

//此处描述超出arrayBufeer位报错
const buffer = new ArrayBuffer(8);//创建了一位8个字节的数据
const i16 = new Int16Array(buffer, 1);
// Uncaught RangeError: start offset of Int16Array should be a multiple of 2


//不通过ArrayBuffer,TypedArray也能够生成内存
const f64a = new Float64Array(8);
f64a[0] = 10;
f64a[1] = 20;
f64a[2] = f64a[0] + f64a[1];

length:length属性表示 TypedArray 数组含有多少个成员。注意将byteLength属性和length属性区分,前者是字节长度,后者是成员长度。

byteOffset:byteOffset属性返回 TypedArray 数组从底层ArrayBuffer对象的哪个字节开始

set:TypedArray 数组的set方法用于复制数组(普通数组或 TypedArray 数组),也就是将一段内容完全复制到另一段内存。

const a = new Uint8Array(8);
const b = new Uint8Array(8);

b.set(a);

subarray:subarray方法是对于 TypedArray 数组的一部分,再建立一个新的视图。subarray方法的第一个参数是起始的成员序号,第二个参数是结束的成员序号(不含该成员),如果省略则包含剩余的全部成员。所以,上面代码的a.subarray(2,3),意味着 b 只包含a[2]一个成员,字节长度为 2。

const a = new Uint16Array(8);
const b = a.subarray(2,3);

a.byteLength // 16
b.byteLength // 2

of:TypedArray 数组的所有构造函数,都有一个静态方法of,用于将参数转为一个TypedArray实例。如

Float32Array.of(0.151, -8, 3.7)

from:静态方法from接受一个可遍历的数据结构(比如数组)作为参数,返回一个基于这个结构的TypedArray实例。

Uint16Array.from([0, 1, 2])
// Uint16Array [ 0, 1, 2 ]

 

3、字节序:

字节序指的是数值在内存中的表示方式。由于 x86 体系的计算机都采用小端字节序(little endian),相对重要的字节排在后面的内存地址,相对不重要字节排在前面的内存地址。(理解此处主要是用来对视图中位数转化时使用,位数转化时由于字节序的不同,拿到的最终数据也不不同

比如,一个占据四个字节的 16 进制数0x12345678,决定其大小的最重要的字节是“12”,最不重要的是“78”。小端字节序将最不重要的字节排在前面,储存顺序就是78563412;大端字节序则完全相反,将最重要的字节排在前面,储存顺序就是12345678。目前,所有个人电脑几乎都是小端字节序,所以 TypedArray 数组内部也采用小端字节序读写数据,或者更准确的说,按照本机操作系统设定的字节序读写数据。

这并不意味大端字节序不重要,事实上,很多网络设备和特定的操作系统采用的是大端字节序。这就带来一个严重的问题:如果一段数据是大端字节序,TypedArray 数组将无法正确解析,因为它只能处理小端字节序!为了解决这个问题,JavaScript 引入DataView对象,可以设定字节序,

const buffer = new ArrayBuffer(16);
const int32View = new Int32Array(buffer);

for (let i = 0; i < int32View.length; i++) {
  int32View[i] = i * 2;
}
// 0 2 4 6

const int16View = new Int16Array(buffer);

for (let i = 0; i < int16View.length; i++) {
  console.log("Entry " + i + ": " + int16View[i]);
}

// Entry 0: 0
// Entry 1: 0
// Entry 2: 2
// Entry 3: 0
// Entry 4: 4
// Entry 5: 0
// Entry 6: 6
// Entry 7: 0


// 假定某段buffer包含如下字节 [0x02, 0x01, 0x03, 0x07]
const buffer = new ArrayBuffer(4);
const v1 = new Uint8Array(buffer);
v1[0] = 2;
v1[1] = 1;
v1[2] = 3;
v1[3] = 7;

const uInt16View = new Uint16Array(buffer);

// 计算机采用小端字节序
// 所以头两个字节等于258  258的二进制为0000000100000010 v1[0]的二进制为00000010 v1[1]的二进制
// 为00000001,由于小端字节序,因此拼接成了258,大端字节序拼接为0000001000000001
if (uInt16View[0] === 258) {
  console.log('OK'); // "OK"
}

4、BYTES_PER_ELEMENT 每一种视图的构造函数,都有一个BYTES_PER_ELEMENT属性,表示这种数据类型占据的字节数。如Int8Array.BYTES_PRE_ELEMENT

5、ArrayBuffer与字符串的转换

// ArrayBuffer 转为字符串,参数为 ArrayBuffer 对象
function ab2str(buf) {
  // 注意,如果是大型二进制数组,为了避免溢出,
  // 必须一个一个字符地转
  if (buf && buf.byteLength < 1024) {
    return String.fromCharCode.apply(null, new Uint16Array(buf));
  }

  const bufView = new Uint16Array(buf);
  const len =  bufView.length;
  const bstr = new Array(len);
  for (let i = 0; i < len; i++) {
    bstr[i] = String.fromCharCode.call(null, bufView[i]);
  }
  return bstr.join('');
}

// 字符串转为 ArrayBuffer 对象,参数为字符串
function str2ab(str) {
  const buf = new ArrayBuffer(str.length * 2); // 每个字符占用2个字节
  const bufView = new Uint16Array(buf);
  for (let i = 0, strLen = str.length; i < strLen; i++) {
    bufView[i] = str.charCodeAt(i);
  }
  return buf;
}

6、溢出规则

  • 正向溢出(overflow):当输入值大于当前数据类型的最大值,结果等于当前数据类型的最小值加上余值,再减去 1。
  • 负向溢出(underflow):当输入值小于当前数据类型的最小值,结果等于当前数据类型的最大值减去余值的绝对值,再加上 1。

 

三、DataView视图

1、作用:

用来对已经创建的ArrayBuffer对象创建混合视图,能够针对当前的片段建立多种不同的类型的数据,该视图方便对各种文件类型解析(文件(如.js,.mp3等)本质上就是通过,定义好的文件结构,生成对应的文件,文件中通常会有很多的隐藏位,用来对文件描述,隐藏位中也通常会采用多种数据类型进行定义)。该视图可以设置字节序。

2、API:

buffer:返回对应的 ArrayBuffer 对象

byteLength:返回占据的内存字节长度

byteOffset:返回当前视图从对应的 ArrayBuffer 对象的哪个字节开始

getInt8、getUint8等:获取一个指定进制的字节。第一个参数指定开始位置,第二个参数为字节序,true为小端字节序,false为大端字节序(默认)

setInt8、setUint8等:设置字节。第一个参数是字节序号,表示从哪个字节开始写入,第二个参数为写入的数据。第三个参数为字节序

四、SharedArrayBuffer(共享视图)

1、作用:ES2017 引入SharedArrayBuffer,允许 Worker 线程与主线程共享同一块内存。SharedArrayBuffer的 API 与ArrayBuffer一模一样,唯一的区别是后者无法共享数据。

// 主线程

// 新建 1KB 共享内存
const sharedBuffer = new SharedArrayBuffer(1024);

// 主线程将共享内存的地址发送出去
w.postMessage(sharedBuffer);

// 在共享内存上建立视图,供写入数据
const sharedArray = new Int32Array(sharedBuffer);



// Worker 线程
onmessage = function (ev) {
  // 主线程共享的数据,就是 1KB 的共享内存
  const sharedBuffer = ev.data;

  // 在共享内存上建立视图,方便读写
  const sharedArray = new Int32Array(sharedBuffer);

  // ...
};

五、Atomics对象

1、作用:

Atomics对象,保证所有共享内存的操作都是“原子性”的,并且可以在所有线程内同步,它可以保证一个操作所对应的多条机器指令,一定是作为一个整体运行的,中间不会被打断。也就是说,它所涉及的操作都可以看作是原子性的单操作,这可以避免线程竞争,提高多线程共享内存时的操作安全。

2、API:

store:用来在共享内存中写入数据,接受三个参数:SharedBuffer 的视图、位置索引和值,返回sharedArray[index]的值

load:用来在共享内存中读取数据,接受两个参数:SharedBuffer 的视图和位置索引,也是返回sharedArray[index]的值

store和load,把多个线程使用共享内存的某个位置作为开关(flag),一旦该位置的值变了,就执行特定操作。这时,必须保证该位置的赋值操作,一定是在它前面的所有可能会改写内存的操作结束后执行;而该位置的取值操作,一定是在它后面所有可能会读取该位置的操作开始之前执行。store方法和load方法就能做到这一点,编译器不会为了优化,而打乱机器指令的执行顺序。

// 主线程 main.js
ia[42] = 314159;  // 原先的值 191
Atomics.store(ia, 37, 123456);  // 原先的值是 163

// Worker 线程 worker.js
while (Atomics.load(ia, 37) == 163);
console.log(ia[37]);  // 123456
console.log(ia[42]);  // 314159

exchange:该方法同store方法一样,都会设置值,不同的是该方法会返回替换前的值

wait:

包括四个参数,第一个参数为共享内存的视图数组,第二个参数为视图数据的位置(从0开始),第三个参数为该位置的预期值。一旦实际值等于预期值,就进入休眠,第四个参数为整数,表示过了这个时间以后,就自动唤醒,单位毫秒。该参数可选,默认值是Infinity,即无限期的休眠,只有通过Atomics.wake()方法才能唤醒。

返回值是一个字符串,共有三种可能的值。如果sharedArray[index]不等于value,就返回字符串not-equal,否则就进入休眠。如果Atomics.wake()方法唤醒,就返回字符串ok;如果因为超时唤醒,就返回字符串timed-out

wake:

包括三个参数,第一个参数为共享内存的视图数组,第二个参数为视图数据的位置(从0开始),第三个参数为需要唤醒的 Worker 线程的数量,默认为Infinity

// 主线程
console.log(ia[37]);  // 163
Atomics.store(ia, 37, 123456);
Atomics.wake(ia, 37, 1);//唤起ia中第37个数据中睡眠的一个线程

// Worker 线程
Atomics.wait(ia, 37, 163);//当ia中第37个数据为163的时候,线程睡眠
console.log(ia[37]);  // 123456

add、sub、and、or、xor:为了避免线程共享问题,因此提供了几个计算方法,同样接受三个参数,共享视图,位置以及值,用于将已经存在的值与传递的新值进行计算。返回旧值

compareExchange:

Atomics.compareExchange(sharedArray, index, oldval, newval):如果sharedArray[index]等于oldval,就写入newval,返回oldval

Atomics.compareExchange的一个用途是,从 SharedArrayBuffer 读取一个值,然后对该值进行某个操作,操作结束以后,检查一下 SharedArrayBuffer 里面原来那个值是否发生变化(即被其他线程改写过)。如果没有改写过,就将它写回原来的位置,否则读取新的值,再重头进行一次操作

isLockFree:

Atomics.isLockFree(size):返回一个布尔值,表示Atomics对象是否可以处理某个size的内存锁定。如果返回false,应用程序就需要自己来实现锁定。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值