JS操作二进制数据的API

390 阅读12分钟

JavaScript 中用于处理二进制数据的API主要有7个:[ArrayBuffer、TypedArray、DataView、Blob、FileReader、URL.createObjectURL、websocket的send方法]下面进行总结
更加标准的文档,请参考:
ArrayBuffer - ECMAScript 6入门
TypedArray - JavaScript | MDN
袁进的博客

一、ArrayBuffer

定义

ArrayBuffer 是一个用于表示通用的、固定长度的二进制数据缓冲区的对象。它在内存中分配了一块连续的字节区域,但不能直接操作这些字节,需要通过 TypedArray 或 DataView 来访问和修改其中的数据。

修改方法

// 创建一个 ArrayBuffer,分配 16 字节的内存 
const buffer = new ArrayBuffer(16); 
// 使用 TypedArray‘s【Uint8Array】 访问 ArrayBuffer 
const uint8Array = new Uint8Array(buffer);
// 向 Uint8Array 中写入数据
uint8Array[0] = 255; 
uint8Array[1] = 128; 
// 使用 DataView 访问 ArrayBuffer 
const dataView = new DataView(buffer); 
// 从 DataView 中读取数据 
const firstByte = dataView.getUint8(0); 
const secondByte = dataView.getUint8(1); 
console.log('Using TypedArray:', uint8Array[0], uint8Array[1]); 
console.log('Using DataView:', firstByte, secondByte);

与字符串之间的转换

ArrayBuffer本身不支持直接与字符串交互。需要通过 TextEncoderTextDecoder 来处理字符串和二进制数据的转换。

const serializedChineseText = '这里是一些中文数据序列化后的结果';   
const encoder = new TextEncoder();
const uint8ArrayData = encoder.encode(serializedChineseText); // 处理 uint8ArrayData 
const decoder = new TextDecoder(); 
const deserializedText = decoder.decode(uint8ArrayData); console.log(deserializedText);

二、TypedArray

定义

TypedArray 是一组构造函数 [Init8Array、Uint8Array、Init16Array、Init32Array、Uint32Array、Float32Array、Float64Arra] 的统称。

Init8Array是8位符号整数,多余的0划给负数,取值范围[-128 ~ 127]
Uint8Array无符号整数取值[0 ~ 255]
当在TypedArray中写入的数据的大小超出其范围时,数据会出现紊乱

特性

  1. TypedArray 实例是存储在ArrayBuffer中的,如果在创建时没有显式传入 ArrayBuffer,它会自动创建一个新的 ArrayBuffer 来存储数据。
// 创建一个长度为 5 的 Uint8Array,会自动创建一个 ArrayBuffer 
const typedArray = new Uint8Array(5); 
// 可以通过 buffer 属性访问底层的 ArrayBuffer 
const underlyingBuffer = typedArray.buffer; 
console.log(underlyingBuffer instanceof ArrayBuffer); 
// 输出: true
  1. 如果多个 TypedArray 实例共享同一个 ArrayBuffer,增删改查都共享。
const buffer = new ArrayBuffer(8); 
const uint8Array1 = new Uint8Array(buffer); 
const uint16Array = new Uint16Array(buffer); 
uint8Array1[0] = 255; 
console.log(uint16Array[0]); //255

作用

1. 操作缓冲区对象

为 ArrayBuffer 提供了一个视图,允许以特定的数据类型(如整数、浮点数)来访问和操作 ArrayBuffer 中的二进制数据。

2. 优化数字数组的存储空间

JS中所有的数字,均使用双精度浮点数保存,64位存一个number,存储10000个0需要10000*64/8/1024=78KB 这时如果改成 Int8Array 存储只需要9KB的存储空间即可。

3. 存储图像、音频等二进制文件的内容

const imageInput = document.getElementById('imageInput');
    const displayImage = document.getElementById('displayImage');
    imageInput.addEventListener('change', function () {
        const file = this.files[0];
        if (file) {
            const reader = new FileReader();
            reader.onload = function (e) {
                const arrayBuffer = e.target.result;
                // 使用 Uint8Array 存储二进制数据
                const uint8Array = new Uint8Array(arrayBuffer);
                // 创建 Blob 对象
                const blob = new Blob([uint8Array], { type: file.type });
                // 创建临时 URL
                const objectURL = URL.createObjectURL(blob);
                // 显示图像
                displayImage.src = objectURL;
            };
            reader.readAsArrayBuffer(file);
        }
    });

4. 结合序列化工具,提高数据传输速度

常结合 Protobuf FlatBuffer 一起使用, 将数据以二进制形式存储在内存中,利用三者的优势,可以显著提高数据处理和传输的效率。
flatBuffers官网

  • 文件编译指令 flatc --js XXX.fbs
  • [flatbuffers编译器下载 (github.com)](github.com/google/flat…
  • flatbuffers 编译器2.x版本之后不再支持js,先编译成ts再tsc为js或者直接用ts
  • 复杂数据先将fbs文件对应的数据结构画出来 使用案例
//city.fbs文件
namespace city_struct;
table city_Struct{
    name:string;
    id:uint64;
    data:[ubyte];
}
root_type city_struct;
include 'BeiDaSplitInfo.fbs' 
table Beijing_Struct{
    code:int64;
    data:[ubyte];
    school_info:SchoolInfo
}
table SchoolInfo{
    cid:int32;
    seq:int32;
    num:int32;
    compress:string;
}
//接收uint8Array
const bbd = new ByteBuffer(uint8Array)
const cityroot = city_Struct.getRootAsCity_Struct(bbd)//调用getRoot
console.log(cityRooot.name())//看转化的js文件里面的方法调
//ubyte作为内层对象getRoot的参数调
const bbd2 = new ByteBuffer(cityroot.dataArray())
//beijing里面的child_info类型是别的分片的别的fbs类型
//先获取beijingRoot和SchoolInfoInfo
const beijingRoot =  Beijing_Struct.getRootAsBeijing_Struct(bbd2)
const schoolRoot = schoolRoot.getRoot(bbd2)//用BeijingRoot的参数
//beijing.js文件里面的school方法参数是SchoolObj,调用把两个root绑一起
beijingRoot.school(schoolRoot)
//学校太多了,分片数据汇总
const totalData = []
if(schoolRoot.num()===schoolRoot.seq()+1){
    totalData.push(...beijingRoot.dataArray())//最后一个分片
}else{
    totalData.push(...Array.from(beijingRoot.dataArray()))
}
//将所有分片信息用protobuf继续反序列化
const total = new uint8Array(totalData)
//protobuf只需要创建实例并deserializeBinary
const pblist = new sList.deserializeBinary(totalData)
console.log(pblist.toObject())

当存储的内容过大,后端常常会进行压缩,如果是zlib压缩,通常先将TypedArray进行压缩以后再解码

export const getCompressData = (data) => {
  return new Promise((resolve, reject) => {
    const zlib = require('zlib')
    zlib.gunzip(Buffer.from(data), (err, res) => {
      if (err) reject(err)
      resolve(res)
    })
  })
}

Node 中的类型化数组 Buffer

  • Buffer继承 Uinit8Array 属于类型化数组
  • 下面的一个字母用一个字节存储
  • Buffer 在使用和输出时可能需要用十六进制表示,下面的两位十六进制表示一个字节
  • Buffer.from 用于从不同的数据源创建一个Buffer实例
const buffer = Buffer.from("abcdefg","utf-8")
console.log(buffer)
// <Buffer 61 62 63 64 65 66 67>
  • 常用于处理服务器net http模块返回的二进制数据
const http = require("http");
const request = http.request(
  "http://yuanjin.tech:5005/api/movie",
  {
    method: "GET",
  },
  (resp) => {
    console.log(resp.statusCode);
    //请求体要按Buffer块读取
    let result = "";
    resp.on("data", (chunk) => {
      result += chunk.toString("utf-8");
      console.log(chunk instanceof Buffer);
    });
    resp.on("end", () => {
      console.log(JSON.parse(result));
    });
  }
);
request.end();

特性

仅存在于Node.js环境中,可以修改值

const buf = Buffer.from([1, 2, 3]);
buf[1] = 100;
console.log(buf); // 输出:<Buffer 01 64 03>

二进制与字符串之间转换方法

//将字符串转换成特定的编码
const buf = Buffer.from("你好", "utf8");//`utf8`、`ascii`、`base64`、`hex`
console.log(buf.toString("base64")); // 输出:5L2g5aW9

// 将二进制转换成字符串
const buffer = Buffer.from([0x48, 0x65, 0x6c, 0x6c, 0x6f]); // 二进制数据:Hello
// 使用 UTF-8 编码将二进制数据转换为字符串
const str = buffer.toString('utf8');
console.log(str); // 输出:Hello

ArrayBuffer 和 Buffer的相互转换

// ArrayBuffer -> Buffer
const arrayBuffer = new ArrayBuffer(3);
const buf = Buffer.from(arrayBuffer);
console.log(buf)
// Buffer -> ArrayBuffer
const buf = Buffer.from([1, 2, 3]);
const arrayBuffer = buf.buffer;
console.log(new Uint8Array(arrayBuffer)); // 输出:Uint8Array(3) [1, 2, 3]

三、DataView

定义

DataView 为 ArrayBuffer 提供了一个抽象的视图,允许你以不同的数据类型(如整数、浮点数等)、不同的字节序(大端序或小端序)来读取和写入 ArrayBuffer 中的数据,而无需考虑底层的字节表示。

实例化

DataView 构造函数的三个参数:

  • buffer:必需,一个 ArrayBuffer 或 SharedArrayBuffer 对象,DataView 将基于该缓冲区进行操作。
  • byteOffset:可选,指定从 ArrayBuffer 的哪个字节开始创建视图,默认值为 0。
  • byteLength:可选,指定视图的字节长度,如果未指定,则视图将包含从 byteOffset 到 ArrayBuffer 末尾的所有字节。
// 创建一个长度为 16 字节的 ArrayBuffer 
const buffer = new ArrayBuffer(16);
// 创建一个 DataView 实例,从第 4 个字节开始,长度为 8 字节 
const dataView = new DataView(buffer, 4, 8);
//buffer属性
console.log(dataView.buffer === buffer); // 输出: true
//byteLength返回字节长度
console.log(dataView.byteLength); // 输出: 8
//byteOffset返回DataView在ArrayBuffer中的起始字节偏移量
console.log(dataView.byteOffset); // 输出: 4

写入和读取方法

  • byteOffset:指定从 ArrayBuffer 的哪个字节开始读取/写入数据。
  • value:要写入的数据。
  • littleEndian:可选,布尔值,指定是否使用小端序,默认值为 false(即使用大端序)。 常见方法
  • setInt8(byteOffset, value):写入一个 8 位有符号整数。
  • setUint8(byteOffset, value):写入一个 8 位无符号整数。
  • setInt16(byteOffset, value, littleEndian):写入一个 16 位有符号整数。
  • setUint16(byteOffset, value, littleEndian):写入一个 16 位无符号整数。
  • getInt8(byteOffset):读取一个 8 位有符号整数。
  • getUint8(byteOffset):读取一个 8 位无符号整数。
  • getInt16(byteOffset, littleEndian):读取一个 16 位有符号整数。
  • getUint16(byteOffset, littleEndian):读取一个 16 位无符号整数。

四、Blob(Binary Large Object)

定义

Blob(Binary Large Object,二进制大对象) 是一个不可变的对象,用于表示可变数据量的二进制数据。它在 Web 开发中被广泛用于处理文件、图片、音频、视频等二进制资源。Blob 对象可以包含多种类型的数据,并且可以通过 FileReaderURL.createObjectURL() 或其他 API 进行读取和操作。

实例化

new Blob(array,options)

  • array:字符串、ArrayBufferTypedArray 或其他 Blob 对象:e.target.files[0] || [string] || [arrayBuffer]]
  • props:MIME和endings Blob对象一旦创建不可修改
// 字符串Blob Array
Blob const text = 'Hello, World!'; 
const blob = new Blob([text], { type: 'text/plain' });
//TypedArray 内容的Blob
const buffer = new ArrayBuffer(8); 
const uint8Array = new Uint8Array(buffer); 
// 向数组中写入数据
for (let i = 0; i < uint8Array.length; i++) { 
uint8Array[i] = i; 
} 
const blobFromArrayBuffer = new Blob([uint8Array], { type: 'application/octet-stream' });
//File是Blob对象的子类
<input type="file" id="fileInput"> 
<script> 
const fileInput = document.getElementById('fileInput'); 
fileInput.addEventListener('change', function () {
    const file = this.files[0];
    if (file) { 
    const newBlob = new Blob([file], { type: file.type }); 
    } 
}); 
</script>

image.png

Blob对象的读取

1. FileReader

const blob = new Blob(['Hello, World!'], { type: 'text/plain' });
const reader = new FileReader(); 
reader.onload = function () { 
    const arrayBuffer = reader.result; 
    console.log(arrayBuffer); 
};
//读取为`ArrayBuffer`,适合处理二进制数据,如图片、音频等
reader.readAsArrayBuffer(blob);
//读取为文本内容,可指定编码格式,默认是`UTF-8`
reader.readAsText(blob);
//读取为`data:` URL 格式的字符串,通常用于在网页中显示图片等
reader.readAsDataURL(blob);

2. 使用Response构造函数

创建一个Response对象,然后使用Response对象的方法来读取Blob内容

const blob = new Blob(['Hello, World!'], { type: 'text/plain' }); 
const response = new Response(blob); 
response.text().then(result => { 
    console.log(result); //获取文本内容
});
//读取ArrayBuffer
response.arrayBuffer()

3. createObjectURL

通过URL.createObjectURL()方法可以创建一个指向Blob对象的临时 URL,然后可以通过该 URL 来访问Blob内容,常用于在img标签中显示图片或在a标签中实现文件下载等

const blob = new Blob(['Hello, World!'], { type: 'text/plain' }); 
const url = URL.createObjectURL(blob); 
const a = document.createElement('a'); 
a.href = url;
a.textContent = '下载文件'; 
a.download = 'test.txt';
document.body.appendChild(a); 
// 释放URL 
URL.revokeObjectURL(url);

Blob与File的关系

File对象的完整原型链

file --> File.prototype --> Blob.prototype --> Object.prototype --> null

当访问 File 对象的属性或方法时,JavaScript 会先在 file 对象本身查找,如果找不到,会依次在 File.prototypeBlob.prototypeObject.prototype 中查找,直到找到该属性或方法,或者到达原型链的顶端(null

// 创建一个 File 对象
const file = new File(['Hello, World!'], 'example.txt', { type: 'text/plain' });
// 检查 File 对象的直接原型 
console.log(Object.getPrototypeOf(file) === File.prototype); 
// 输出: true 
// 检查 File 对象的原型链上是否包含 Blob.prototype 
console.log(Object.getPrototypeOf(File.prototype) === Blob.prototype); 
// 输出: true

五、FileReader

创建

构造函数无参 new FileReader() 在浏览器中,由于安全限制,不能直接用FileReader读取相对路径的文件,也不能使本地的相对路径的文件转换成Blob想要读取本地的文件只能通过用户交互。

读取步骤

在下列读取方法中写入Blob或者File,在onload中得到读取的结果,可以将读取结果进行以下处理

  1. readAsArrayBuffer()读取文件ArrayBuffer对象
    • 可以使用new Uint8Array(result)来将arrayBuffer转换成指定类型的TypedArray
  2. readAsDataUrl()返回base64字符串
  3. readAsText()读取字符串
  4. readAsBinaryString()原始二进制文件
<body>
  <input type="file" id="fileInput">
  <script>
    const fileInput = document.getElementById('fileInput');
    fileInput.addEventListener('change', function (event) {
      const file = event.target.files[0];
      const reader = new FileReader();
      reader.readAsArrayBuffer(file);
      reader.onload = function () {
        const arrayBuffer = reader.result;
        const uint8Array = new Uint8Array(arrayBuffer);
        console.log(uint8Array);
      };
    });
  </script>
</body>

六、URL

定义

URL - Web API | MDN

实例化

new URL(url, base),其中 url 是要解析的 URL 字符串,base 是可选的基本 URL,用于处理相对 URL

//单个参数
const url = new URL(location.href)
console.log(url)
//全部参数
const url = new URL('../cats', 'http://www.example.com/dogs');
console.log(url.toString()); // "www.example.com/cats"
console.log(url.pathname); // "/cats"

实例方法

  1. toString():返回完整的 URL 字符串。
  2. toJSON():返回完整的 URL 字符串。

静态方法

URL.createObjectURL(object)

  • 用于创建一个指向指定对象(如 BlobFile)的 URL。这个 URL 是一个全局唯一的字符串,类似于 blob:https://example.com/unique-id,它允许网页在不将数据上传到服务器的情况下,直接访问本地文件或内存中的数据。
  • 参数object:可以是一个 Blob 对象、File 对象,或者任何实现了 Blob 接口的对象。
  • 返回一个字符串,表示指向对象的 URL。
使用场景
  1. 预览本地文件 在用户选择文件后,无需上传到服务器即可直接在页面中预览。
const fileInput = document.querySelector('input[type="file"]');
fileInput.addEventListener('change', (event) => {
    const file = event.target.files[0];
    const objectURL = URL.createObjectURL(file);
    const img = document.createElement('img');
    img.src = objectURL;
    document.body.appendChild(img);
});
  1. 动态生成文件并下载
const blob = new Blob(["Hello, world!"], { type: "text/plain" });
const objectURL = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = objectURL;
a.download = "hello.txt";
a.click();
  1. <audio><video> 中播放本地文件
const fileInput = document.querySelector('input[type="file"]');
fileInput.addEventListener('change', (event) => {
    const file = event.target.files[0];
    const objectURL = URL.createObjectURL(file);
    const audio = document.createElement('audio');
    audio.src = objectURL;
    audio.controls = true;
    document.body.appendChild(audio);
});
类似可作为 src 属性的 Base64

和URL创建的objectURL类似,base64 也可以作为链接进行预览
创建方法

  1. btoa()将字符串转换成base64
const str = "Hello";
const base64 = btoa(str); // 输出:SGVsbG8=
console.log(base64);
  1. fileReader.readAsDataURL(blob|file)将二进制数据(如图片)转换为 Base64
const fileInput = document.querySelector('input[type="file"]');
fileInput.addEventListener('change', (event) => {
    const file = event.target.files[0];
    const reader = new FileReader();
    reader.onload = (e) => {
        const base64 = e.target.result.split(',')[1]; // 去掉数据类型前缀
        console.log(base64);
    };
    reader.readAsDataURL(file);
});
  1. canvas.toDataURL()//构造器重载,参见.toDataURL()| MDN (mozilla.org),将Canvas转换成Base64实现效果预览
<!DOCTYPE html>
<html>
<head>
  <style>
    canvas {
      border: 1px solid black;
    }
  </style>
</head>
<body>
  <canvas id="signatureCanvas" width="400" height="200"></canvas>
  <button onclick="clearCanvas()">清除</button>
  <button onclick="downloadSignature()">下载</button>
  <img id="signatureImage" width="400" height="200">
  <script>
    const canvas = document.getElementById('signatureCanvas');
    const ctx = canvas.getContext('2d');
    const signatureImage = document.getElementById('signatureImage');
    ctx.strokeStyle = 'black';
    ctx.lineWidth = 2;
    let drawing = false;
    canvas.addEventListener('mousedown', (e) => {
      drawing = true;
      ctx.beginPath();
      ctx.moveTo(e.clientX - canvas.offsetLeft, e.clientY - canvas.offsetTop);
    });
    canvas.addEventListener('mousemove', (e) => {
      if (!drawing) return;
      ctx.lineTo(e.clientX - canvas.offsetLeft, e.clientY - canvas.offsetTop);
      ctx.stroke();
    });
    canvas.addEventListener('mouseup', () => {
      drawing = false;
    });
    function clearCanvas() {
      ctx.clearRect(0, 0, canvas.width, canvas.height);
      signatureImage.src = '';
    }
    function downloadSignature() {
      const dataURL = canvas.toDataURL('image/png');
      const link = document.createElement('a');
      link.href = dataURL;
      link.download = 'signature.png';
      signatureImage.src = canvas.toDataURL('image/png');
      link.click();
    }
  </script>
</body>

</html>

URL.revokeObjectURL(objectURL)

  • 用于撤销之前通过 URL.createObjectURL() 创建的对象 URL。撤销后,该 URL 将不再有效,浏览器会释放与之关联的资源。
  • objectURL:需要撤销的对象 URL,必须是之前通过 URL.createObjectURL() 创建的。 两个静态方法经常搭配一块使用,避免内存资源的浪费
const fileInput = document.querySelector('input[type="file"]');
let currentObjectURL = null;

fileInput.addEventListener('change', (event) => {
    if (currentObjectURL) {
        URL.revokeObjectURL(currentObjectURL);
    }

    const file = event.target.files[0];
    currentObjectURL = URL.createObjectURL(file);
    const img = document.querySelector('img');
    img.src = currentObjectURL;
});

七、websocket 的二进制传输

WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议,它支持二进制数据的传输。可以通过 WebSocket 的 send() 方法发送 ArrayBuffer 或 Blob 类型的数据

// 创建一个 WebSocket 连接
const socket = new WebSocket('ws://example.com'); 
// 连接建立后发送二进制数据
socket.addEventListener('open', function () {
    const buffer = new ArrayBuffer(16); 
    const uint8Array = new Uint8Array(buffer); 
    uint8Array[0] = 255; 
    socket.send(buffer); 
    }
);
// 接收二进制数据
socket.addEventListener('message', function (event) { 
    const arrayBuffer = event.data;
    console.log(arrayBuffer); 
    }
);