ArrayBuffer 是什么,TypedArray 与 DataView 视图又是什么?

ArrayBuffer

ArrayBuffer: ArrayBuffer 对象用来表示通用的原始二进制数据缓冲区,接口原始设计目的与 WebGL 有关 WebGL: 指浏览器与显卡之间的通信接口

  • 为了满足 JS 与显卡之间大量、实时的数据交互,它们之间的数据通信必须是二进制的,而不能是传统的文本格式
  • 如文本格式传递一个 32 位整数,两端的 JS 脚本与显卡都要进行格式转化,将会非常耗时
  • 所以如果可以将 4 个字节的 32 位整数,以二进制形式原封不动地送入显卡,脚本的性能就会大幅提升
  • 二进制数组就是在这种背景下诞生的 → 它很像 C 语言的数组,允许开发者以数组下标的形式,直接操作内存,大大增强了 Js 处理二进制数据的能力,使得开发者有可能通过 Js 与操作系统的原生接口进行二进制通信

二进制数组主要由这三类对象组成: ArrayByffer & TypedArray & DataView

  • ArrayBuffer 对象: 代表内存之中的一段二进制数据,可以通过 "视图" 进行操作,"视图" 部署了数组接口,这意味着,可以用数组的方法操作内存 → tip: ArrayBuffer 只能通过下面的两个视图对象进行操作

  • TypedArray 视图: 共包括 9 种类型的视图

    • 数据类型字节长度含义对应的 C 语言类型
      Int818 位带符号整数signed char
      Uint818 位不带符号整数unsigned char
      Uint8C18 位不带符号整数(自动过滤溢出unsigned char
      Int16216 位带符号整数short
      Uint16216 位不带符号整数unsigned short
      Int32432 位带符号整数int
      Uint32432 位不带符号的整数unsigned int
      Float32432 位浮点数float
      Float64864 位浮点数double
  • DataView 视图: 可以自定义复合格式的视图,比如第一个字节是 Uint8(无符号 8 位整数)、第二、三个字节是 Int16(16 位整数)、第四个字节开始是 Float32(32 位浮点数)等等,此外还可以自定义字节序

  • 概括: ArrayBuffer 对象代表原始的二进制数据 | TypedArray 视图用来读写简单类型的二进制数据 | DataView 视图用来读写复杂类型的二进制数据

创建 ArrayBuffer 对象: new ArrayBuffer(byteLength) | new ArrayBuffer(byteLength,options)

  • const buffer = new ArrayBuffer(32) // --> 创建一个 32 字节的缓冲区(内存区域) → 每个字节默认都为 0
    
  • // 可通过第二个参数 options 中的 maxByteLength 来配置最大字节数
    const buffer = new ArrayBuffer(32, { 
        maxByteLength: 64 // --> 创建一个 32 字节的缓冲区,但允许最大扩展到 64 字节
    })
    

通过视图进行基本读写操作: 举例

  • TypedArray

    • const buffer = new ArrayBuffer(16) // --> 创建一个 16 字节的缓冲区(内存区域) → 每个字节默认都为 0
      
      const typed_array1 = new Int32Array(buffer) // -->  创建一个指向 buffer 的 Int32 视图(4字节)
      console.log(buffer, typed_array1)
      
      typed_array1[0] = 100 // --> 通过视图,修改第一个元素
      console.log(typed_array1, typed_array1[0])
      
    • // TypedArray视图的构造函数,除了接受ArrayBuffer实例作为参数,还可以接受普通数组作为参数,直接分配内存生成底层的ArrayBuffer实例,并同时完成对这段内存的赋值
      const typed_array2 = new Int32Array([100, 0, 0, 0]) // --> 创建一个包含 4 个元素的 Int32 视图
      console.log(typed_array2); // -- 与上面一致
      
  • DataView

    • // --> 创建一个指向 buffer 的 DataView 视图 → DataView视图的创建,需要提供ArrayBuffer对象实例作为参数
      const buffer = new ArrayBuffer(16)
      const dataview = new DataView(buffer) 
      console.log(dataview)
      
      // -- 在 DataView 中通过对应的 getInt32 方法,可以读取指定位置的字节,并返回一个 32 位有符号整数,通过 setInt32 方法,可以写入一个 32 位有符号整数到指定位置(其它 int8 等同理)
      dataview.setInt32(0, 100, true)
      console.log(dataview.getInt32(0, true)) 
      
      //DataView 实例提供了以下方法来读取内存,参数是对应的字节序号(索引),表示读取的字节位置
      
    • 具体使用在下面示例

实例属性: 原型

  • byteLength: 属性返回该数组缓冲区的长度(以字节为单位)
  • detached: 属性返回一个布尔值,指示该缓冲区是否已经分离(传输) ArrayBuffer 被创建时该值位 false,当 ArrayBuffer 已被传输时该值就变为 true → 对应的实例也会从内存中分离 → 变为不可用
  • maxByteLength: 属性返回该数组缓冲区可调整到的最大长度(以字节为单位)
  • resizable: 属性返回此数组缓冲区是否可以调整大小

实例方法: 原型

  • resize: 方法将 ArrayBuffer 调整为指定的大小,以字节为单位

    • const buffer = new ArrayBuffer(12, { maxByteLength: 32 })
      console.log(buffer.byteLength) // log: 12
      
      buffer.resize(16) // -- 调整缓冲区大小为 16 字节
      console.log(buffer.byteLength) // log: 16;
      
  • slice: 方法用于将内存区域的一部分,拷贝生成一个新的 ArrayBuffer 对象 切片

    • const buffer = new ArrayBuffer(8);
      const newBuffer = buffer.slice(0, 3); // -- slice(start,end) : [start.end)
      
    • ``slice 方法其实包含两步,第一步是先分配一段新内存,第二步是将原来那个ArrayBuffer对象拷贝过去

  • transfer & transferToFixedLength: 实验性方法,自行查阅文档

视图

视图:

  • ArrayBuffer 对象作为内存区域,可以存放多种类型的数据 → 同一段内存,不同数据有不同的解读方式,这就叫做“视图”(view)
  • ArrayBuffer 有两种视图,一种是 TypedArray 视图,另一种是 DataView 视图
TypedArray 视图

TypedArray 视图: 目前 TypedArray 视图一共包括 9 种类型,每一种视图都是一种构造函数 → 这 9 个构造函数生成的数组,统称为 TypedArray 视图

  • 数据类型字节长度含义对应的 C 语言类型
    Int818 位带符号整数signed char
    Uint818 位不带符号整数unsigned char
    Uint8C18 位不带符号整数(自动过滤溢出unsigned char
    Int16216 位带符号整数short
    Uint16216 位不带符号整数unsigned short
    Int32432 位带符号整数int
    Uint32432 位不带符号的整数unsigned int
    Float32432 位浮点数float
    Float64864 位浮点数double
  • 与普通数组的差异:

    • + TypedArray 数组的所有成员,都是同一种类型
      + TypedArray 数组的成员是连续的,不会有空位
      + TypedArray 数组成员的默认值为 0
      + TypedArray 数组只是一层视图,本身不储存数据,它的数据都储存在底层的 ArrayBuffer 对象之中,要获取底层对象必须使用 buffer 属性
      

TypedArray 视图构造函数的几种用法

  • 1. TypedArray(buffer, byteOffset=0, length?): 同一个 ArrayBuffer 对象之上,可以根据不同的数据类型,建立多个视图

    • 参数:
      	+ 第一个参数(必需):视图对应的底层 ArrayBuffer 对象
      	+ 第二个参数(可选):视图开始的字节序号,默认从 0 开始
      	+ 第三个参数(可选):视图包含的数据个数,默认直到本段内存区域结束
      
    • // 创建一个8字节的ArrayBuffer
      const b = new ArrayBuffer(8);
      
      // 创建一个指向b的Int32视图,开始于字节0,直到缓冲区的末尾 → 每个元素占用4个字节,所以对应 v1 的长度为 2
      const v1 = new Int32Array(b);
      
      // 创建一个指向b的Uint8视图,开始于字节2,直到缓冲区的末尾 → 每个元素占用 1 个字节,并且偏移两个字节,所以对应 v2 的长度为 6
      const v2 = new Uint8Array(b, 2);
      
      // 创建一个指向b的Int16视图,开始于字节2,长度为2 → 每个元素占用 2 个字节,并且偏移两个字节与视图包含数据的个数位 2,所以对应 v3 的长度为 2
      const v3 = new Int16Array(b, 2, 2);
      
      console.log(v1.length, v2.length, v3.length) // 输出:2 6 2
      
      
      // -- 长度解析 ↓
         v1
          + 开始于字节 0,直到缓冲区的末尾
          + 每个元素占用 4 个字节
          + 由于 ArrayBuffer 的长度是 8 字节,可以容纳 232 位整数,所以 v1 的长度为 2 
      
        v2
          + 开始于字节 2,直到缓冲区的末尾
          + 每个元素占用 1 个字节
          + 由于偏移了 2 个字节,并且 ArrayBuffer 的长度是 8 字节,所以 v2 的长度为 68 - 2 = 6)
      
        v3
          + 开始于字节 2,长度为 2
          + 每个元素占用 2 个字节
          + 由于偏移了 2 个字节,并且长度为 2,所以 v3 的长度为 2
      
    • 🔺只要任何一个视图对内存有所修改,对应其它也共享着该 ArrayBuffer 对应内存的视图上也反应出来 上面也提到了,因为视图本身不储存数据,当修改对应数据时,它的数据都是储存在底层的 ArrayBuffer 对象之中

      • console.log(v1, v2, v3);
        v3[0] = 100 // -- 修改 v3 视图中的第一个元素
        console.log(v1, v2, v3); // -- 由于 v1 和 v2 也共享着该 v3 视图中的第一个元素的 ArrayBuffer 中的数据,所以对应的内存空间的元素也会进行改变
        
    • 注意: byteOffset 必须与所要建立的数据类型一致,否则会报错 如下示例

      • const buffer = new ArrayBuffer(8);
        const i16 = new Int16Array(buffer, 1);
        // Uncaught RangeError: start offset of Int16Array should be a multiple of 2
        
        ↑ 因为 i16 视图每个元素是占 2 个字节的,对应的 byteOffset 参数必须能够被 2 整除 → 即偏移对应元素的字节(如 1 3 5 等,就会抛出错误)
        
        + 如果想从任意字节开始解读 ArrayBuffer 对象,必须使用 DataView 视图,因为 TypedArray 视图只提供 9 种固定的解读格式
        
  • 2. TypedArray(length): 视图可以不通过 ArrayBuffer 对象,直接分配内存生成

    • const f32a = new Float32Array(2) // --> 创建一个包含 2 个元素的 Float32Array 视图(每个元素4字节)
      console.log(f32a)
      f32a[0] = 1.1
      f32a[1] = f32a[0] * 2
      console.log(f32a)
      
      ↑ 生成 2 个成员的 Float32Array 数组(共8个字节)
      
  • 3. TypedArray(typedArray): 接收另外一个 TypedArray 作为参数

    • const x1 = new Uint8Array(4)
      const typedArray = new Int8Array(x1) // -- 该 typedArray 根据 x1 生成一个新的 4 给元素的视图(每个元素 1 字节)
      
    • 注意,此时生成的新数组,只是复制了参数数组的值,对应的底层内存是不一样的 → 新数组会开辟一段新的内存储存数据,不会在原数组的内存之上建立视图(所以当某一个视图修改内容时,另一个是不会跟着改变的,因为是一块独立的内存空间)

    • 如果想基于同一段内存,构造不同的视图,我们需要通过传入的参数位另一个 TypedArray 中的 buffer 属性作为参数 → buffer 属性,用于在视图中获取当前底层存储对象 ArrayBuffer

      • const x = new Int8Array([1, 1]);
        const y = new Int8Array(x.buffer); 
        
  • 4. TypedArray(arrayLikeObject): 接收一个普通数组,生成对应的 TypedArray 视图

    • const typedArray = new Uint8Array([1, 2, 3, 4]); // -- 此 TypedArray 视图会重新开辟内存,不会在原数组的内存上建立视图
      
    • TypedArray 数组也可以转换回普通数组

      • const normalArray = [...typedArray];
        // or
        const normalArray = Array.from(typedArray);
        // or
        const normalArray = Array.prototype.slice.call(typedArray);
        

字节序: 字节序指的是数值在内存中的表示方式

  • 示例:

    • const int32view = new Int32Array([0, 2, 4, 8]) // --> [0, 2, 4, 8]
      
      const int16view = new Int16Array(int32view.buffer) // -- 在上面 32 位整数的视图视图的基础上,创建一个 16 位整数的视图
      console.log(int16view) // --> [0, 0, 2, 0, 4, 0, 8, 0]
      
  • 我们来思考一下在 int32view 数据上接着建立一个 16 位整数的视图,则读出完全不一样的结果,这是为什么?

    • 1. 由于每个 16 位整数占据 2 个字节,所以整个 int32view 中的 buffer 对象被分成了 8 段 → 由于 x86 体系的计算机都采用小端字节
    • 2. 在小端字节(little endian)中,相对重要的字节会排在后面的地址,相对不重要字节排在前面的内存地址,所以就得到了上面的结果,如下图例
    • 3. 所以,也会以 0 2 4 8 进行排列,如果是大端字节时就会以 8 4 2 0 进行排列(对应的 int16view = 80 40 20 00)
  • ArrayBuffer 与字符串的互相转换

实例属性: 原型

  • + length: 属性返回 TypedArray 由多少个成员 → 注意与 byteLength 属性的区分,前者成员长度,后者字节长度
    

实例方法: 原型

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

    • // 复制整个数组 set(arr)
      const a = new Uint8Array(8);
      const b = new Uint8Array(8);
      b.set(a); // -- 将 a 中的内容复制到 b 中的内存中
      
    • // 复制数组中的某一项 set(arr,index) → 方法还可以接受第二个参数,表示从b对象的哪一个成员开始复制a对象
      const a = new Uint16Array(8);
      const b = new Uint16Array(10);
      b.set(a, 2) // -- 上面代码的 b 数组比 a 数组多两个成员,所以从 b[2] 开始复制
      
      
  • slice: TypeArray 实例的 slice 方法,可以返回一个指定位置的新的 TypedArray 实例

实例静态方法

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

    • Float32Array.of(0.151, -8, 3.7) // -- 传入的对应参数,会作为数组的每一项,创建处对应的 TypedArray 对象
      // Float32Array [ 0.151, -8, 3.7 ]
      
    • // 方法一
      let tarr = new Uint8Array([1,2,3]);
      
      // 方法二
      let tarr = Uint8Array.of(1,2,3);
      
      // 方法三
      let tarr = new Uint8Array(3);
      tarr[0] = 1;
      tarr[1] = 2;
      tarr[2] = 3;
      
  • from: 静态方法 from 接受一个可遍历的数据结构(比如数组)作为参数,返回一个基于这个结构的 TypedArray 实例

    • Uint16Array.from([0, 1, 2])
      // Uint16Array [ 0, 1, 2 ]
      
      const ui16 = Uint16Array.from(Uint8Array.of(0, 1, 2));
      ui16 instanceof Uint16Array // true
      
    • // from 方法还可以接受一个函数,作为第二个参数,用来对每个元素进行遍历,功能类似 map 方法
          Int8Array.of(127, 126, 125).map(x => 2 * x)
          // Int8Array [ -2, -4, -6 ] -- 溢出
      
          Int16Array.from(Int8Array.of(127, 126, 125), x => 2 * x)
          // Int16Array [ 254, 252, 250 ]
      
    • 上面的例子中,from 方法没有发生溢出,这说明遍历不是针对原来的 8 位整数数组 → 也就是说,from 会将第一个参数指定的 TypedArray 数组,拷贝到另一段内存之中,处理之后再将结果转成指定的数组格式

复合视图: 由于视图的构造函数可以指定起始位置和长度,所以在同一段内存之中,可以依次存放不同类型的数据,这叫做“复合视图”

  • const buffer = new ArrayBuffer(24);
    const idView = new Uint32Array(buffer, 0, 1);
    const usernameView = new Uint8Array(buffer, 4, 16);
    const amountDueView = new Float32Array(buffer, 20, 1);
    
  • 上面代码将一个 24 字节长度的ArrayBuffer对象,分成三个部分

    • 字节 0 到字节 3:1 个 32 位无符号整数
    • 字节 4 到字节 19:16 个 8 位整数
    • 字节 20 到字节 23:1 个 32 位浮点数
DataView 视图

DataView 视图: 如果一段数据包括多种类型(比如服务器传来的 HTTP 数据),这时除了建立ArrayBuffer对象的复合视图以外,还可以通过DataView视图进行操作

创建视图

  • const buffer = new ArrayBuffer(24);
    const dv = new DataView(buffer);
    

实例读取方法

  • 方法描述
    getnt8读取 1 个字节,返回一个 8 位整数
    getUint8读取 1 个字节,返回一个无符号的 8 位整数
    getInt16读取 2 个字节,返回一个 16 位整数
    getUint16读取 2 个字节,返回一个无符号的 16 位整数
    getInt32读取 4 个字节,返回一个 32 位整数
    getUint32读取 4 个字节,返回一个无符号的 32 位整数
    getFloat32读取 4 个字节,返回一个 32 位浮点数
    getFloat64读取 8 个字节,返回一个 64 位浮点数
  • const buffer = new ArrayBuffer(24);
    const dv = new DataView(buffer);
    
    // 从第1个字节读取一个8位无符号整数
    const v1 = dv.getUint8(0);
    
    // 从第2个字节读取一个16位无符号整数
    const v2 = dv.getUint16(1);
    
    // 从第4个字节读取一个16位无符号整数
    const v3 = dv.getUint16(3);
    
  • // 小端字节序
    const v1 = dv.getUint16(1, true);
    
    // 大端字节序
    const v2 = dv.getUint16(3, false);
    
    // 大端字节序
    const v3 = dv.getUint16(3);
    

实例写入方法

  • 方法描述
    setInt8写入 1 个字节的 8 位整数
    setUint8写入 1 个字节的 8 位无符号整数
    setInt16写入 2 个字节的 16 位整数
    setUint16写入 2 个字节的 16 位无符号整数
    setInt32写入 4 个字节的 32 位整数
    setUint32写入 4 个字节的 32 位无符号整数
    setFloat32写入 4 个字节的 32 位浮点数
    setFloat64写入 8 个字节的 64 位浮点数
  • // 在第1个字节,以大端字节序写入值为25的32位整数
    dv.setInt32(0, 25, false);
    
    // 在第5个字节,以大端字节序写入值为25的32位整数
    dv.setInt32(4, 25);
    
    // 在第9个字节,以小端字节序写入值为2.5的32位浮点数
    dv.setFloat32(8, 2.5, true);
    
  • 如果不确定正在使用的计算机的字节序,可以采用下面的判断方式

    • const littleEndian = (function() {
        const buffer = new ArrayBuffer(2);
        new DataView(buffer).setInt16(0, 256, true);
        return new Int16Array(buffer)[0] === 256;
      })();
      
    • 如果返回 true,就是小端字节序;如果返回 false,就是大端字节序

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值