博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
二进制数组的操作
阅读量:5879 次
发布时间:2019-06-19

本文共 12149 字,大约阅读时间需要 40 分钟。

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)) // trueconsole.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)) // 5218const unit8 = new Uint8Array(unit16.buffer)console.log(unit8[0].toString(16)) // 18console.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 0console.log(v2.byteLength, v2.byteOffset) // 2 2console.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) // 4console.log(v1.byteLength) // 8复制代码

TypedArray.prototype.set

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

const v1 = new Uint8Array(4)v1[0] = 1v1[1] = 2v1[2] = 3v1[3] = 4const 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] = 1v1[1] = 2v1[2] = 3v1[3] = 4const 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)) // 0console.log(dv.getInt16(1)) // 0console.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)) // 18console.log(dv.getUint8(1).toString(16)) // 52console.log(dv.getUint8(2).toString(16)) // 90console.log(dv.getUint8(3).toString(16)) // 6econsole.log(dv.getUint8(4).toString(16)) // c9console.log(dv.getUint8(5).toString(16)) // 6cconsole.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)) // 5218console.log('源'.codePointAt(0).toString(16)) // 6e90console.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学习之路很有很长

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

转载地址:http://nfcix.baihongyu.com/

你可能感兴趣的文章
[翻译]Protocol Buffer 基础: C++
查看>>
runloop与线程的关系
查看>>
[Bzoj2246]迷宫探险(概率+DP)
查看>>
[译] 感受 4px 基线网格带来的便利
查看>>
oracle常用函数
查看>>
MYBATIS
查看>>
详解消息队列的设计与使用
查看>>
iOS 项目优化
查看>>
筛选出sql 查询结果中 不包含某个字符
查看>>
8进制与16进制
查看>>
使用Sqoop从mysql向hdfs或者hive导入数据时出现的一些错误
查看>>
mybatis:Invalid bound statement (not found)
查看>>
电脑中毒的现象
查看>>
django表单操作之django.forms
查看>>
webSocket vnc rfb
查看>>
列表推导式 生成器表达式
查看>>
控制子窗口的高度
查看>>
Linux 防火墙iptables命令详解
查看>>
打造笔记本电脑基地重庆要当全球“老大”
查看>>
处理 Oracle SQL in 超过1000 的解决方案
查看>>