浅析Uint8Array语法及常见使用、Uint8Array.slice与Uint8Array.subarray区别(是否指向同一个内存空间)、new Uint8Array(typedArray)构造函数对typedArray的引用问题(保持同一个引用)、Uint8Array与String互相转换

一、Uint8Array 介绍

  Uint8Array 数组类型表示一个8位无符号整型数组,创建时内容被初始化为0。创建完后,可以以对象的方式或使用数组下标索引的方式引用数组中的元素。

  详细介绍见 MDN 描述:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array

  我们要使用了解的话,主要看下示例代码:

// 来自长度
var uint8 = new Uint8Array(2);
uint8[0] = 42;
console.log(uint8[0]); // 42
console.log(uint8.length); // 2
console.log(uint8.BYTES_PER_ELEMENT); // 1

// 来自数组
var arr = new Uint8Array([21,31]);
console.log(arr[1]); // 31

// 来自另一个 TypedArray
var x = new Uint8Array([21, 31]);
var y = new Uint8Array(x);
console.log(y[0]); // 21

// 来自 ArrayBuffer
var buffer = new ArrayBuffer(8);
var z = new Uint8Array(buffer, 1, 4);
z; // Uint8Array(4) [0, 0, 0, 0]
// 来自一个迭代器 var iterable = function*(){ yield* [1,2,3]; }(); var uint8 = new Uint8Array(iterable); // Uint8Array[1, 2, 3]

二、Uint8Array.slice 与 Uint8Array.subarray 区别

  最近在处理 png 图像解码时,使用到Uint8Array对象。发现该对象在部分android浏览器中没有slice方法。翻了一遍API文档,对象的slicesubarray方法字面描述基本一样。于是使用subarray方法取而代之。结果自然是很悲催了,解析出每帧数据都一样(解码过程中有去修改对象数据)。

  到这里大概已经猜到了subarrayslice的区别就在于内存空间占用上。为探个究竟,跑个简单的demo来验证下:

let a = new Uint8Array([1,2,3,4,5,6]);

let b = a.subarray(3,5);
let c = a.slice(3,5);

// 将b的第一个值改为9
b[0] = 9;

console.log('a',a);
// 输出:a Uint8Array(6) [1, 2, 3, 9, 5, 6]
console.log('b',b);
// 输出:b Uint8Array(2) [9, 5]
console.log('c',c);
// 输出:c Uint8Array(2) [4, 5]

  果然,修改ba对应的值也是随之变化的,说明是在同一内存空间上。而c不与前者内存共享,是在独立的空间上。

  问题是找到了,解决办法就自然简单了。方法有无数种。检查原型是否有对应的方法肯定是必不可少的。如果原型上没有slice就自行往原型上添加一个即可。循环效率较低,这里还是决定使用原型本身的subarray来处理。最后解决问题的兼容代码如下:

// 兼容代码,如果原型上无`slice`则添加一个
if(!Uint8Array.prototype.slice){
    Uint8Array.prototype.slice = function(...arg){
        return new Uint8Array(this).subarray(...arg);
    }
};

let a = new Uint8Array([1,2,3,4,5,6]);
let b = b.slice(3,5);

console.log('a',a);
// 输出:a Uint8Array(6) [1, 2, 3, 4, 5, 6]

console.log('b',b);
// 输出:b Uint8Array(2) [4, 5]

  这里其实也比较简单,就是新建了一个 Unit8Array 然后去 subarray()。

三、Uint8Array 构造函数对 typedArray 的引用问题

  Javascript 的 Uint8Array 支持字节数据,对于操作二进制数据非常有用,笔者初次接触时发现它有几个构造函数,如下:

new Uint8Array(); 
new Uint8Array(length);
new Uint8Array(typedArray);
new Uint8Array(object);
new Uint8Array(buffer [, byteOffset [, length]]);

  这些函数都返回一个 Uint8Array 类型的对象,但对于 new Uint8Array(typedArray); 这个形式的构造函数需要理解一下。在MDN的官方文档里描述不详:

   new Uint8Array(typedArray) 表示根据 typedArray 提供的对象创建一个 Uint8Array 对象,并保持对typedArray对象的引用,这个形式的构造函数不会复制typedArray对象的。尝试以下代码,看看输出结果。

// create a TypedArray with a size in bytes
var buffer = new ArrayBuffer(8);
 
var typedArray1 = new Uint8Array(buffer);
typedArray1[0] = 32;
typedArray1[1] = 33;
typedArray1[2] = 34;
 
var typedArray2 = new Uint8Array(buffer);
typedArray2[0] = 42;
typedArray2[1] = 43;
typedArray2[2] = 44;
 
console.log(typedArray1);
//不是输出 Uint8Array [32, 33, 34, 0, 0, 0, 0, 0]
//正确输出 Uint8Array [42, 43, 44, 0, 0, 0, 0, 0]
 
console.log(typedArray2);
//正确输出 Uint8Array [42, 43, 44, 0, 0, 0, 0, 0]
 

  从以上代码看到了 typedArray1 与 typedArray2 输出完全一样。原因就是 typedArray1.buffer 与 typedArray2.buffer 指向的是同一个对象,因此分别修改 typedArray1 与 typedArray2 时,实际上修改的是同一内存对象。

When creating an instance of a TypedArray (e.g. Int8Array), an array buffer is created internally in memory or, if an ArrayBuffer object is given as constructor argument, then this is used instead.  The buffer address is saved as an internal property of the instance and all the methods of %TypedArray%.prototype, i.e. set value and get value etc., operate on that array buffer address.

  因此,需要注意的是:用 ArrayBuffer 作为构造函数的参数时,Uint8Array直接引用这个ArrayBuffer对象作为内部缓冲,而不再创建内部ArrayBuffer对象。

四、Uint8Array 与 String 互转

1、字符串转Uint8Array

function stringToUint8Array(str){
  var arr = [];
  for (var i = 0, j = str.length; i < j; ++i) {
    arr.push(str.charCodeAt(i));
  }
  var tmpUint8Array = new Uint8Array(arr);
  return tmpUint8Array
}

2、Uint8Array转字符串

function Uint8ArrayToString(fileData){
  var dataString = "";
  for (var i = 0; i < fileData.length; i++) {
    dataString += String.fromCharCode(fileData[i]);
  }
  return dataString
}