二进制数组的操作

2023-11-11

ES6之前是不能通过代码直接操作二进制数据的,为了方便开发者可以直接操作二进制数据,ES6提出了三个操作二进制数据的接口:ArrayBuffer、TypedArray和DataView

ArrayBuffer

ArrayBuffer代表储存二进制数据的一段内存,但是它不能直接读写,只能通过视图进行读写

比如我们想创建一段32字节的内存数据

const buf = new ArrayBuffer(32)
复制代码

创建好了之后我们想要读写这段内存,我们需要通过视图,如:

const buf = new ArrayBuffer(32)
const bufView = new Float64Array(buf)
console.log(bufView) // Float64Array [ 0, 0, 0, 0 ]
复制代码

Float64Array是TypedArray视图的一种,表示64位浮点数(8个字节)的视图

除了使用TypedArray创建视图之外,我们还可以通过DataView

const buf = new ArrayBuffer(32)
const bufView = new DataView(buf)
console.log(bufView.getUint8(0)) // 0
复制代码

关于DataView和TypedArray的区别我们下面会介绍,现在你只需要知道TypedArray不是某个具体的构造函数,而是代表了一组构造函数,而DataView则是一个构造函数

ArrayBuffer.prototype.byteLength

返回实例所分配的内存区域的字节长度

const buf = new ArrayBuffer(32)
console.log(buf.byteLength) // 32
复制代码

ArrayBuffer.prototype.slice

允许将内存区域的一部分复制成生成一个新的ArrayBuffer对象 ,用法同Array的slice

const buf = new ArrayBuffer(6)
const bufView = new Uint16Array(buf)
bufView[0] = '刘'.codePointAt(0)
bufView[1] = '源'.codePointAt(0)
bufView[2] = '泉'.codePointAt(0)
const buf2 = buf.slice(0)
const bufView2 = new Uint16Array(buf2)
for(let i of bufView2) {
  console.log(String.fromCodePoint(i))
}
// 刘
// 源
// 泉
复制代码

上面这段代码复制buf对象的所有字节,生成一个新的ArrayBuffer对象buf2,buf2和buf互相不影响,属于两块内存区域

ArrayBuffer.isView

isView是一个静态方法,表示参数是否是ArrayBuffer的视图实例

const buf = new ArrayBuffer(6)
const bufView = new Uint16Array(buf)
console.log(ArrayBuffer.isView(bufView)) // true
console.log(ArrayBuffer.isView(buf)) // false
复制代码

TypedArray

TypedArray视图有九种类型,每种类型的数组成员都是同一个数据类型

  • Int8Array:8位有符号整数,长度一个字节
  • Uint8Array:8位无符号整数,长度一个字节
  • Unit8ClampedArray:8位无符号整数,长度一个字节,溢出处理不同
  • Int16Array:16位有符号整数,长度为2个字节
  • Uint16Array:16位无符号整数,长度为2个字节
  • Int32Array:32位有符号整数,长度为4个字节
  • Uint32Array:32位无符号整数,长度为4个字节
  • Float32Array:32位浮点数,长度为4个字节
  • Float64Array:64位浮点数,长度为8个字节

构造函数

TypedArray(buffer, byteOffset = 0, length?)
第一个参数必选:视图对应的底层ArrayBuffer对象
第二个参数可选:视图开始的字节序号,默认是0
第三个参数可选:视图包含的数据个数,默认到本段内存区域结束

const buf = new ArrayBuffer(4)

const bufView = new Uint8Array(buf, 1, 1)
复制代码

注意:byteOffset必须与所建立的数据类型一致,否则会报错

const buf = new ArrayBuffer(4)

const bufView = new Uint16Array(buf, 1)
// RangeError: start offset of Uint16Array should be a multiple of 2
复制代码

TypedArray(length)
视图还可以不通过ArrayBuffer对象,直接分配生成

const bufView = new Uint16Array(2)
bufView[0] = '徐'.codePointAt(0)
bufView[1] = '洁'.codePointAt(0)
for(let s of bufView) {
  console.log(String.fromCodePoint(s))
}
// 徐
// 洁
复制代码

TypedArray(typedArray)
可以接受另一个TypedArray实例作为参数,此时生成新的TypedArray实例和传入的TypedArray实例,两者对应的底层内存区域不一样,二者互相不影响

const bufView = new Uint16Array(3)
bufView[0] = '徐'.codePointAt(0)
bufView[1] = '洁'.codePointAt(0)
const bufView2 = new Uint16Array(bufView)
bufView2[0] = '刘'.codePointAt(0)
bufView2[1] = '源'.codePointAt(0)
bufView2[2] = '泉'.codePointAt(0)
console.log(bufView) // Uint16Array [ 24464, 27905, 0 ]
console.log(bufView2) // Uint16Array [ 21016, 28304, 27849 ]
复制代码

TypedArray(arrayLikeObject)
也可以接受一个类数组,这时候生成的TypedArray实例会开辟新的内存,而不会在类数组的内存上建立视图

const obj = {length: 3}
const bufView = new Uint16Array(obj)
bufView[0] = '刘'.codePointAt(0)
bufView[1] = '源'.codePointAt(0)
bufView[2] = '泉'.codePointAt(0)
console.log(obj) // { length: 3 }
console.log(bufView) // Uint16Array [ 21016, 28304, 27849 ]
复制代码

要将一个TypedArray转化为一个普通数组可以调用Array.prototype.slice方法

const normalArray = [].slice.call(typedArray)
复制代码

BYTES_PER_ELEMENT

每一种视图的构造函数都有一个BYTES_PER_ELEMENT属性,表示这种数据类型占据的字节数

字符串与ArrayBuffer互相转化

在JavaScript中字符串采用UTF-16编码,即一个字符用两个字节存储,我们可以编写转化函数

字符串转ArrayBuffer

const str2ab = str => {
  const buf = new ArrayBuffer(str.length * 2)
  const bufView = new Uint16Array(buf)
  for(let i = 0, l = str.length;i < l;i++) {
    bufView[i] = str.codePointAt(i)
  }
  return buf
}
复制代码

ArrayBuffer转字符串

const ab2str = buf => {
  return String.fromCodePoint.apply(null, new Uint16Array(buf))
}
复制代码

TypedArray.prototype.buffer

TypedArray的实例的buffer属性返回整段内存区域对应的ArrayBuffer对象,该属性只读

const unit16 = new Uint16Array(1)
unit16[0] = '刘'.codePointAt(0)
console.log(unit16[0].toString(16)) // 5218
const unit8 = new Uint8Array(unit16.buffer)
console.log(unit8[0].toString(16)) // 18
console.log(unit8[1].toString(16)) // 52
复制代码

TypedArray.prototype.byteLength & TypedArray.prototype.byteOffset

byteLength返回TypedArray数组占据的内存长度,单位为字节
byteOffset返回TypedArray数组从底层ArrayBuffer对象的哪个字节开始。两个属性都只读

const buf = new ArrayBuffer(8)

const v1 = new Uint8Array(buf)
const v2 = new Uint16Array(buf, 2, 1)
const v3 = new Uint32Array(buf, 4, 1)

console.log(v1.byteLength, v1.byteOffset) // 8 0
console.log(v2.byteLength, v2.byteOffset) // 2 2
console.log(v3.byteLength, v3.byteOffset) // 4 4
复制代码

TypedArray.prototype.length

length标书TypedArray数组还有多少成员

const buf = new ArrayBuffer(8)

const v1 = new Uint16Array(buf)

console.log(v1.length) // 4
console.log(v1.byteLength) // 8
复制代码

TypedArray.prototype.set

用于复制数组,也就是将一段内存完全复制到另一段内存

const v1 = new Uint8Array(4)
v1[0] = 1
v1[1] = 2
v1[2] = 3
v1[3] = 4
const v2 = new Uint8Array(4)
const v3 = new Uint8Array(6)
v2.set(v1)
v3.set(v1, 2)
console.log(v2) // Uint8Array [ 1, 2, 3, 4 ]
console.log(v3) // Uint8Array [ 0, 0, 1, 2, 3, 4 ]
复制代码

同时我们还可以对set指定第二个参数,表示从target哪一个成员开始复制,默认是0

TypedArray.prototype.subarray & TypedArray.prototype.slice

subarray和slice用法一模一样,用法同Array.slice。当参数为-1表示倒数第一个位置,-2表示倒数第二个位置,以此类推

const v1 = new Uint8Array(4)
v1[0] = 1
v1[1] = 2
v1[2] = 3
v1[3] = 4
const v2 = v1.subarray(0, 2)
const v3 = v1.subarray(-1)
const v4 = v1.slice(1, 3)
const v5 = v1.slice(-1)
console.log(v2) // Uint8Array [ 1, 2 ]
console.log(v3) // Uint8Array [ 4 ]
console.log(v4) // Uint8Array [ 2, 3 ]
console.log(v5) // Uint8Array [ 4 ]
复制代码

TypedArray.of

静态方法,用于将参数转为一个TypedArray实例

const v1 = Uint16Array.of('刘'.codePointAt(0), '源'.codePointAt(0), '泉'.codePointAt(0))
console.log(v1) // Uint16Array [ 21016, 28304, 27849 ]
复制代码

我们也可以这样初始化一个TypedArray实例

const v1 = new Uint16Array(['刘'.codePointAt(0), '源'.codePointAt(0), '泉'.codePointAt(0)])

console.log(v1) // Uint16Array [ 21016, 28304, 27849 ]
复制代码

或者

const v1 = new Uint16Array(3)
v1[0] = '刘'.codePointAt(0)
v1[1] = '源'.codePointAt(0)
v1[2] = '泉'.codePointAt(0)
console.log(v1) // Uint16Array [ 21016, 28304, 27849 ]
复制代码

TypedArray.from

静态方法,接受一个类数组,返回一个基于此结构TypedArray实例,用法可参考Array.from

const v1 = Uint16Array.from({length: 3})
console.log(v1) // Uint16Array [ 0, 0, 0 ]
复制代码

还可以将一种TypedArray实例转化为另一种

const v1 = Uint16Array.from(Uint8Array.of(1, 2, 3))
console.log(v1) // Uint16Array [ 1, 2, 3 ]
console.log(v1.byteLength) // 6
复制代码

from还可以接收一个函数作为第二个函数,用来对每个元素进行遍历,功能类似map

const int8 = Int8Array.of(127, 126, 125).map(x => x * 2)

console.log(int8) // Int8Array [ -2, -4, -6 ] 发生溢出

const int16 = Int16Array.from(Int8Array.of(127, 126, 125), x => x * 2)

console.log(int16) // Int16Array [ 254, 252, 250 ] 没有溢出
复制代码

第一个发生了溢出,这个很显然,因为Int8Array的每个成员表示的范围为-128-127,

而第二个没有发生溢出,说明from会将第一个参数指定的数组先复制到另一块内存中,然后在对结果进行处理,并不是直接对第一个参数指定的数组进行遍历

DataView

用于处理数据成员是多种类型的情况,除此之外还支持字节序
初始化一个DataView对象

const buf = new ArrayBuffer(5)

const dv = new DataView(buf)
复制代码

DataView实例下有buffer,byteLength,byteOffset含义和用法同TypedArray

DataView实例提供了8个方法用于读取内存

  • getInt8,读取一个字节,返回一个8位整数
  • getUint8,读取一个字节,返回一个无符号的8位整数
  • getInt16,读取二个字节,返回一个16位整数
  • getUint16,读取二个字节,返回一个无符号的16位整数
  • getInt32,读取四个字节,返回一个32位整数
  • getUint32,读取四个字节,返回一个无符号的32位整数
  • getFloat32,读取四个字节,返回一个32位浮点数
  • getFloat64,读取八个字节,返回一个64位浮点数

这一系列的get方法的参数都是一个字节序号,不允许为负,表示从哪个字节开始读取

const buf = new ArrayBuffer(10)

const dv = new DataView(buf)

console.log(dv.getInt8(0)) // 0
console.log(dv.getInt16(1)) // 0
console.log(dv.getInt32(2)) // 0
复制代码

当一次读取两个以上字节的数据,需要明确数据的存储方式,小端字节序还是大端字节序,默认采用大端字节序存储(false)

const str2ab = str => {
  const buf = new ArrayBuffer(str.length * 2)
  const bufView = new Uint16Array(buf)
  for(let i = 0, l = str.length;i < l;i++) {
    bufView[i] = str.codePointAt(i)
  }
  return buf
}

const str = '刘源泉'
const buf = str2ab(str)

for(let i of new Uint16Array(buf)) {
  console.log(i.toString(16))
}

console.log('-----------------')
const dv = new DataView(buf)
console.log(dv.getUint8(0).toString(16)) // 18
console.log(dv.getUint8(1).toString(16)) // 52
console.log(dv.getUint8(2).toString(16)) // 90
console.log(dv.getUint8(3).toString(16)) // 6e
console.log(dv.getUint8(4).toString(16)) // c9
console.log(dv.getUint8(5).toString(16)) // 6c

console.log(dv.getUint16(0).toString(16)) // 1852 大端字节序
console.log(dv.getUint16(0, false).toString(16)) // 1852 大端字节序
console.log(dv.getUint16(0, true).toString(16)) // 5218 小端字节序
复制代码

同样的写入内存也提供了八个方法

  • setInt8,写入1个字节的8位整数
  • setUint8,写入1个字节的8位无符号整数
  • setInt16,写入2个字节的16位整数
  • setUint16,写入2个字节的16位无符号整数
  • setInt32,写入4个字节的32位整数
  • setUint32,写入4字节的32位无符号整数
  • setFloat32,写入4个字节的32位浮点数
  • setFloat64,写入8个字节的64位浮点数

这一系列的set方法,接受2个参数:第1个参数是字节序号,第2个参数表示写入的数据,当写入两个字节以上的数据时,需要提供第三个参数,表示数据的存储方式,默认是大端字节序(false)

const buf = new ArrayBuffer(32)

const dv = new DataView(buf)

console.log('刘'.codePointAt(0).toString(16)) // 5218
console.log('源'.codePointAt(0).toString(16)) // 6e90

console.log('---------')

dv.setUint16(0, '刘'.codePointAt(0), true) // 小端字节序写入
dv.setUint16(2, '源'.codePointAt(0), false) // 大端字节序写入

console.log(dv.getUint16(0, false).toString(16)) // 1852 // 大端字节序读取
console.log(dv.getUint16(0, true).toString(16)) // 5218 // 小端字节序读取

console.log(dv.getUint16(2, false).toString(16)) // 6e90 // 大端字节序读取
console.log(dv.getUint16(2, true).toString(16)) // 906e // 小端字节序读取
复制代码

如何判断计算机使用的字节序,可以用下面这个方法

const littleEnidan = (() => {
  const buf = new ArrayBuffer(2)
  const dv = new DataView(buf)
  dv.setInt16(0, 0x0001, true)
  return new Int16Array(buf)[0] === 0x0001
})()

console.log(littleEnidan)
复制代码

如果返回true,表示小端字节序,否则是大端字节序

二进制数组的应用

API讲的差不多了,该说下二进制数组的应用

XHR2

XHR2允许服务器返回二进制数据,当我们知道服务器会返回二进制数据,我们需要设置responseType为arraybuffer,请求成功之后response就是返回给我们的二进制数据,我们需要创建视图去进行读写操作

const xhr = new XMLHttpRequest()
xhr.open('GET', url)
xhr.responseType = 'arraybuffer'
xhr.onreadystatechange = function() {
    if (xhr.readyState === 4 && xhr.status === 200) {
        const arraybuffer = xhr.response
        // 二进制数组处理
    }
}
xhr.send()
复制代码

Canvas

Canvas中操作像素的方法有三个createImageData、getImageData和putImageData

而像素数据是Unit8ClampedArray数组,因为Unit8ClampedArray的溢出处理比其他TypedArray处理起来更方便,确保小于0的值设为0,大于255的值设为255

下面给出两个Canvas操作像素的demo

一个是随着鼠标的移动动态改变文字的颜色

const canvas = document.getElementById('canvas')
const h1 = document.getElementById('h1')
const ctx = canvas.getContext('2d')
const img = new Image()
img.onload = function() {
    ctx.drawImage(img, 0, 0, 300, 150)
    canvas.addEventListener('mousemove', function(event) {
      const x = event.layerX
      const y = event.layerY
      const imageData = ctx.getImageData(x, y, 1, 1)
      const unit8 = imageData.data
      const color = `rgba(${unit8[0]}, ${unit8[1]}, ${unit8[2]}, ${unit8[3]})`
      h1.style.color = color
      h1.textContent = color
    })
}
img.src = 'url'
复制代码

另外一个是图片灰度和颜色反相

const canvas = document.getElementById('canvas')
const invertbtn = document.getElementById('invertbtn')
const grayscalebtn = document.getElementById('grayscalebtn')
const ctx = canvas.getContext('2d')
const img = new Image()
img.onload = function() {
    ctx.drawImage(img, 0, 0, 300, 150)
    const imageData = ctx.getImageData(0, 0, 300, 150)
    const unit8 = imageData.data
    invertbtn.addEventListener('click', function() {
      for(let i = 0;i < unit8.length; i += 4) {
        unit8[i] = 255 - unit8[i]
        unit8[i + 1] = 255 - unit8[i + 1]
        unit8[i + 2] = 255 - unit8[i + 2]
      }
      ctx.putImageData(imageData, 0, 0)
    })
    grayscalebtn.addEventListener('click', function() {
      for(let i = 0;i < unit8.length; i += 4) {
        const avg = (unit8[i] + unit8[i + 1] + unit8[i + 2]) / 3
        unit8[i] = avg
        unit8[i + 1] = avg
        unit8[i + 2] = avg
      }
      ctx.putImageData(imageData, 0, 0)
    })
}
复制代码

Canvas对图片处理就是对二进制像素数据的运算,至于该怎么进行运算大家可以到网上找下相关的运算规则,比如亮度,灰度,透明度等

Fetch

Fetch取回的数据就是ArrayBuffer对象

fetch(url)
.then(request => request.arrayBuffer())
.then(arrayBuffer => {})
复制代码

File

当我们上传图片时,我们可以使用FileReader将图片读取成ArrayBuffer,然后可以使用视图对ArrayBuffer进行处理,处理完成之后在上传到服务器或展示在其它Canvas元素中

const input = document.getElementById('input')
const read = document.getElementById('read')
read.addEventListener('click', function(e) {
const reader = new FileReader()
reader.addEventListener('load', processimage, false)
    reader.readAsArrayBuffer(input.files[0])
})
const processimage = function(e) {
    const buffer = e.target.result
    const dv = new DataView(buffer)
    // 二进制数组处理
}
复制代码

最后

关于二进制数组的应用其实还有两个:SharedArrayBuffer和WebSocket。后面讲到这两点的时候在补上,大家如果感兴趣的话可以查阅相关资料。

JavaScript学习之路很有很长

你们的打赏是我写作的动力


本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

二进制数组的操作 的相关文章

随机推荐

  • 网站发布一般步骤以及解决方法

    1 在D盘 随便一个地方 新建文件夹 2 在vs项目中点击发布弹出对话框 3 配置文件选择自定义 4 下一步 Publish method 选择file system 5 target location选择第一步创建的文件夹 6 下一步 f
  • 《软件测试的艺术》

    1 每当测试一个程序时 应当想到要为程序增加一些价值 通过测试来增加程序的价值 是指测试提高了程序的可靠性或质量 提高了程序的可靠性 是指找出并最终修改了程序的错误 因此 不要只是为了证明程序能够正确运行而去测试程序 相反 应该一开始就假设
  • datetime数据类型在页面上的显示不完全

    下面两个代码全包含在script标签中 function fmtDate sDate var dt new Date sDate var y dt getFullYear var m dt getMonth 1 var d dt getDa
  • 用VS2015开发Linux程序

    1 开发工具 VS2015Update3 Visual C for Linux Development VC Linux exe 下载链接 介绍 VMware 虚拟机软件 ubuntu 16 04 desktop amd64 iso Lin
  • C# 预处理器指令(学习心得 24)

    预处理器指令 指导编译器在实际编译开始之前对信息进行预处理 所有的预处理器指令都是以 开始 在一行上 只有空白字符可以出现在预处理器指令之前 预处理器指令不是语句 所以它们不以 分号 结束 一个预处理器指令必须是该行上的唯一指令 超级小白友
  • Mysql 一主多备安装部署文档

    Mysql 一主多备安装部署文档 文章目录 Mysql 一主多备安装部署文档 1 主节点配置 1 1 my cnf 配置 1 2 配置同步账号 1 3 授权同步账号 1 4 授权远程登录 1 5 刷新 1 6 查看Master状态 2 Sl
  • vmware workstation 16 player 导出虚拟机ovf文件

    vmware workstation 16 player 导出虚拟机ovf文件 1 找到vm的ovftool 位于C Program Files x86 VMware VMware Player OVFTool 2 找到虚拟机对应 vmx文
  • MATLAB对csv文件的某一列数据进行数据处理

    clc clear all close all M csvread shui A Aref csv 1 2 N csvread kongA Aref csv 1 2 baseline 1 mean M 1 16 baseline 2 mea
  • UWB的定位算法(简单详细易懂)

    系列文章目录 文章目录 系列文章目录 前言 一 控制部分 二 UWB 的测距原理是什么 三 TOF 数学计算 四 Trilateration 三边测量法的原理与计算方法 TDOA平面 1 三边测量法的缺陷是 2 Z 轴准确度比 X 轴 Y
  • 多项目同时进行,如何做好项目管理?

    大部分企业在运营过程中一般会存在多个项目并行推进的情况 一段时间只运营一个项目的情况已经很少 无论是对项目管理者还是项目执行者而言 多项目同时进行比单项目运行更具挑战 多项目管理一般会存在各项目之间抢资源 资源冲突 资源分配不合理 可能存在
  • 使用node-forge pki进行RSA加密

    先放npm官方文档 www npmjs com package node forge 在知道RSA加密的大致原理后 再往下看 使用例子 简单写个方法 引入依赖 import forge from node forge base64转换 一般
  • 第十届蓝桥杯决赛B组:排列数

    这题我们用动态规划做 首先我们来找规律 对于一个递增的数列 如123456 我们插入一个数 这个数大于数列中所有的数 这里插入7 如果不插在两端 1 6 的数两侧 则增加了两个拐点 如1273456 插在 1 6 的内测 有两种情况 如17
  • win2008+IIS7.5+VS2013+4.5netframework,HTTP 错误 404.0 - Not Found 错误代码 0x80070002 解决办法

    win2008系统IIS7 5部署网站后访问首页正常 但访问其他地址时出错 如 访问http localhost ARCIMS Website lanzfc veiwers htm出错 错误如下 应用程序 DEFAULT WEB SITE
  • [论文阅读笔记77]LoRA:Low-Rank Adaptation of Large Language Models

    1 基本信息 题目 论文作者与单位 来源 年份 LoRA Low Rank Adaptation of Large Language Models microsoft International Conference on Learning
  • 2019新年flag

    多的不说了 直接立flag吧 看看年底的时候完成情况 dubbo的细节回顾结合dubbo面试题进行学习 netty的项目总结和源码学习 es的源码学习 系统学习 结合脑图 要有输出 数量不在多 在于精 多运动 多读书 少看直播
  • UI Automation编程辅助工具Inspect的下载和使用

    UIAutomation微软提供的UI自动化库 主要用AutomationElement类来表示UI 自动化目录树中的一个UI自动化元素 NET Windows的窗体应用程序和WPF应用程序 Inspect是一款类似于SPY的界面捕捉工具
  • 拉普拉斯的原理

    拉普拉斯是一种二阶导数算子 是一个与方向无关的各向同性 旋转轴对称 边缘检测算子 若只关心边缘点的位置而不顾其周围的实际灰度差时 一般选择该算子进行检测 拉普拉斯算子为二阶差分 其方向信息丢失 常产生双像素 对噪声有双倍加强作用 因此它很少
  • ng-model指令

    ng model指令作用是绑定HTML表单元素到AngularJS应用程序数据中 即 scope变量中 语法
  • Ispci命令详解

    说明 lspci 是一个用来显示系统中所有PCI总线设备或连接到该总线上的所有设备的工具 参数 v 使得 lspci 以冗余模式显示所有设备的详细信息 vv 使得 lspci 以过冗余模式显示更详细的信息 事实上是 PCI 设备能给出的所有
  • 二进制数组的操作

    ES6之前是不能通过代码直接操作二进制数据的 为了方便开发者可以直接操作二进制数据 ES6提出了三个操作二进制数据的接口 ArrayBuffer TypedArray和DataView ArrayBuffer ArrayBuffer代表储存