Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ArrayBuffer DataView 数据存储方式 字节序处理 #7

Open
pkjy opened this issue Jun 14, 2020 · 0 comments
Open

ArrayBuffer DataView 数据存储方式 字节序处理 #7

pkjy opened this issue Jun 14, 2020 · 0 comments

Comments

@pkjy
Copy link
Owner

pkjy commented Jun 14, 2020

Javascript在数据的处理上一直不是强项,比如数字不分整型和浮点数统一使用了64位浮点数,如果涉及到二进制运算则显得非常无力,在数据传输上也非常浪费带宽。在ES6针对Javascript二进制数据处理上的无力引入了原始缓冲区ArrayBuffer,并且还提供了多种位数的int类型数组以及数据视图来处理数据。

ArrayBuffer & DataView

图解

用一个 Int8 的确定类型数组来分离存放 8 位二进制字节。
image
用一个无符号的 Int16 数组来分离存放 16 位二进制字节,这样如果是一个无符号的整数也能处理。
image
甚至可以在相同基础的 buffer 上使用不同的 view,同样的操作不同的 view 会给你不同的结果。

比如,如果我们在这个 ArrayBuffer 中从 Int8 view 里获取了元素 0 和 1,在 Uint16 view 中元素 0 会返回给我们不同的值,尽管它们包含的是完全相同的二进制字节。
image
在这种方式中,ArrayBuffer 基本上扮演了一个原生内存的角色,它模拟了像 C 语言才有的那种直接访问内存的方式。

你可能想知道为什么我们不让程序直接访问内存,而是添加了这种抽象层。直接访问内存将导致一些安全漏洞。

更多ArrayBuffer的基础介绍可参考 《JavaScript 标准参考教程》-阮一峰-二进制数组

数据的存储方式

一个整型数字写成2进制之后以书写习惯来说左边是高位,右边是低位。举个例子,int8的5使用二进制是这么表示的:

0000 0101

最左边的0是符号位,表示非负数,101则是5的二进制写法。

负数无法使用非负数的规则来表示,比如-1用非负数的规则表示是:

1000 0001

考虑到-1如果加上1值应该为0,那么该值加上1却会变成-2:

1000 0010

也就是在计算方式上出现问题了,所以二进制是采用了补码的方式来表示,补码可以完全不考虑符号位,它的规则如下:

  • 非负数直接用正常的二进制数表示
  • 负数是绝对值二进制取反加1

按这种规则,-1的绝对值二进制是:

0000 0001

取反后是:

1111 1110

再加1则是:

1111 1111

我们再尝试给它加上1,看看值是多少:

1 0000 0000

明显高位溢出了,于是将高位多余的1截掉变成:

0000 0000

它的值如我们所料是0,这就是补码。uint8由于没有正负之分,它的所有值都是非负数,所以就不需要考虑补码。

数据溢出截断

首先需要了解一下int16用二进制是如何表示的,以3850为例子,它的二进制表示方式是这样子的:

00001111 00001010

int16占了两个字节,因此它被截成两断,我们把左边的字节称为高位字节,右边的字节称为低位字节。在内存中它是如何存放的呢?建立一个Int8Array来看一下:

var buffer = new ArrayBuffer(2);
var int16Array = new Int16Array(buffer);
var int8Array = new Int8Array(buffer);
int16Array[0] = 3850;
// Int16Array [ 3850 ]
console.log(int16Array);
// Int8Array [ 10, 15 ]
console.log(int8Array);

从运行结果看,3850被截断成两个int8值:

int8Array[0] = 10 = 00001010
int8Array[1] = 15 = 00001111

看起来很反人类是吧?高位字节不是放在左边下标为0的字节上,却放在右边下标为1的字节上,跟我们的书写习惯反过来了!

事实上,数据在内存中存储并没有硬性规则高位字节必须放在左边(虽然它更符合人类阅读习惯),具体实现也是根据当前CPU的实现。大多数计算机是以高位优先的顺序存放数据(即高位在左,低位在右),但基于Intel CPU的计算机则是反过来以低位优先存放数据,我的本子就是Intel CPU的,因此我的运行结果就是Int8Array [ 10, 15 ],或许换个电脑就会变成Int8Array [ 15, 10 ]。

那么当数据溢出截断又是怎么处理的?尝试着给一个int8字节赋值3850,看看最终得到的值是什么:

var int8Array = new Int8Array([3850]);
//Int8Array [ 10 ]
console.log(int8Array);

很明显,它把15抛弃了,也就是保存了低位字节,抛弃高位。事实上,溢出处理在各种CPU上都是保留低位能保留的字节,把高位的截断,这个与数据存储是按高位优先还是低位优先没什么关系。

字节序处理

请先看以下示例代码在我机子上跑的情况:

var buffer = new ArrayBuffer(2);
var view = new DataView(buffer);
var int16Array = new Int16Array(buffer);
view.setInt8(0, 10);
// 10
console.log(view.getInt8(0))
// 2560 用二进制表示是:00001010 00000000,
console.log(view.getInt16(0));
int16Array[0] = 10;
// 2560!
console.log(view.getInt16(0));

仔细看结果,很明显,view使用的是高位优先的读写方式,而TypeArray使用的是低位优先的读写方式。在内存中两个字节的数据在setInt8(0, 10)的时候会变成

00001010 00000000

按照我计算机的读写规则应该是低位优先,也就是实际上这里应该被解读成

00000000 00001010

也就是得到10,但实际上使用view却得到了2560,也就是高位优先。

DataView默认是使用big-endian方式,也就是高位优先进行读写。但在读写多字节数据的时候,可以通过传入值为true的little-endian参数来要求使用低位优先规则读写数据。

参考链接

1 《JavaScript 标准参考教程》-阮一峰-二进制数组
2 缓冲数组以及数据视图
3 通俗漫画介绍 ArrayBuffers 和 SharedArrayBuffers

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant