写在前面
- 周一和小姐妹 Skady 宝贝练完舞后,交流了一个小时技术,是的🙆我们没有扯皮,难得交流技术。🤣她在玩“在C语言中编写JS代码,然后再编译成wasm,可以在浏览器里跑”,我们简单地交流了一下底层实现.应该是基于 ArrayBuffer 的
- 然后不知怎么就想到了 0.1 + 0.2 ≠ 0.3,这个经典问题
- 想看看它在内存中的表现
- 并记录一下将 String 字符串,转成字节流的整个实现过程
- 往下看吧~ 🤓
实践过程
总体思路
- 统计字符串长度
- 循环遍历字符串,进行编码
- 输出编码后的字节流
function UTF8_Encoding(str) {
let length = str.length,
strIndex = -1,
strEnd = length - 1,
bytes = new Uint8Array(length),
index = 0;
while (strIndex++ < strEnd) {
bytes[index] = str.charCodeAt(strIndex) & 0xFF; // 设置为低8位值
index += 1;
}
return bytes;
}
字节流文件转 String
总体思路
- 统计字节流长度
- 循环遍历字节流,进行解码
- 输出解码后的字节流
function UTF8_Decoding(bytes) {
let str = "",
bytesIndex = -1,
bytesEnd = bytes.length - 1;
while (bytesIndex ++ < bytesEnd) {
str += String.fromCharCode(bytes[bytesIndex]);
}
return str;
};
测试截图
类型化数组
-
类型化数组中的元素都是数字。使用构造函数在创建类型化数组的时候决定了数组中数字(有符号或者无符号整数或者浮点数)的类型和大小(以位为单位)
-
类型化数组有固定的长度。
-
在创建类型化数组的时候,数组中的元素总是默认初始化为0.
-
类型化数组
-
TypedArray 视图支持的数据类型一共有 9 种( DataView 视图支持除 Uint8C 以外的其他 8 种)。
类型 字节长度 含义 对应的 C 语言类型 t8 1 8 位带符号整数 signed char nt8 1 8 位不带符号整数 unsigned char nt8C 1 8 位不带符号整数(自动过滤溢出) unsigned char t16 2 16 位带符号整数 short nt16 2 16 位不带符号整数 unsigned short t32 4 32 位带符号整数 int nt32 4 32 位不带符号的整数 unsigned int oat32 4 32 位浮点数 float oat64 8 64 位浮点数 double
-
-
在创建一个类型化数组的时候,可以传递数组大小给构造函数,或者传递一个数组或者类型化数组来用于初始化数组元素。一旦创建了类型化数组,就可以像操作其他类数组对象那样,通过常规的中括号表示法来对数组元素进行读/写操作。
var bytes = new Uint8Array(1024); // 1kb字节 for (var i = 0; i < bytes.length; i ++) // 循环数组的每个元素 for (var i = 0; i < bytes.length; i ++) // 循环数组的每个元素 bytes[i] = i & 0xFF; // 设置为索引的低8位值 var copy = new Uint8Array(bytes); // 创建数组的副本 var ints = new Int32Array([0, 1, 2, 3]); // 包含这4个int值的类型化数组
-
类型化数组:他们都是基本字节块的视图,称为一个 ArrayBuffer 。ArrayBuffer 只是不透明的字节块。可以通过类型化数组获取这些字节,但是 ArrayBuffer 自己并不是一个类型化数组。可以像对任意 Javascript 对象那样,使用数字数组索引来操作 ArrayBuffer。但是,这样做并不能赋予访问缓冲区中字节的权限
var bytes = new Uint8Array(8); // 分配8个字节 bytes[0] = 1 // 把第一个字节设置为1 bytes.buffer[0] = 255 // 错误获取,缓冲区中没有索引值0 bytes.buffer[1] = 255 // 错误设置缓冲区字节
“字节顺序”
-
字节组织成更长的字的顺序
-
为了高效,类型化数组采用底层硬件的原生顺序。在低位优先(little-endian)系统中, ArrayBuffer 中数字的字节是按照从低位到高位的顺序排列的。在高位优先(big-endian)系统中,字节是按照从高位到低位的顺序排列的。
-
可以使用如下代码来检测系统的字节顺序:
// 如果整数 Ox00000001 在内存中表示成: 01 00 00 00 // 则说明当前系统是低位优先系统 // 相反,在高位优先系统中,它会表示成:00 00 00 01 var little_endian = new Int8Array(new Int32Array([1]).buffer)[0] === 1
思考问题 0.1 + 0.2 ≠ 0.3
- 我们可以先来看看 十进制小数的二进制表示:
- 整数部分:除以2,取出余数,商继续除以2,直到得到0为止,将取出的余数逆序
- 小数部分:乘以2,然后取出整数部分,将剩下的小数部分继续乘以2,然后再取整数部分,一直取到小数部分为零为止。如果永远不为零,则按要求保留足够位数的小数,最后一位做0舍1入。将取出的整数顺序排列。
- 所以 0.1 可以表示为
- 0.000110011001100110011…
- 0.2 可以表示为
- 0.00110011001100110011…
// 0.1 和 0.2 都转化成二进制后再进行运算
0.00011001100110011001100110011001100110011001100110011010 +
0.0011001100110011001100110011001100110011001100110011010 =
0.0100110011001100110011001100110011001100110011001100111
// 转成十进制正好是 0.30000000000000004
-
二进制数转成 IEEE 754 标准
-
-
使用 64 位固定长度来表示,也就是标准的double 双精度浮点数。
-
这样的存储结构优点是可以归一化处理整数和小数,节省存储空间。
-
这64个比特又可分为三个部分,即:
- 第1位: 是符号的标志位(S), 0代表正数,1代表负数
- 第1-11位: 指数位(E), 存储指数(exponent),用来表示次方数
- 第12-63位: 尾数(M), 这52 位是尾数,超出的部分自动进一舍零
-
实际数字就可以用以下公式来计算:
-
因此 0.1 的二进制表示:
0.00011001100110011001100110011001100110011001100110011001100…
-
首先 0.1 是正数,标志位
Sign = 0
-
其次, 将小数转化为科学计数法
1.100110011001100110011001100110011001100110011001100… * 2^-4
-
相对于,小数点移了4位,指数减4
exponent = -4 + 1023 = 1019
01111111011
-
-
由于科学计数法, 第一个数始终是1, 所以可以忽略存储, 只要存后面的52位就可以了
-
如果超过了52位, 就是对第53位舍0进1(精度误差), 结果也就是
1001100110011001100110011001100110011001100110011010
-
-
因此 0.1 的 IEEE 754 的表示:
-
同理 0.2 的 IEEE 754 的表示:
-
查看 0.1 表示方式
let b = new Float64Array([0.1])
console.log(b)
let intArr = new Uint8Array(b.buffer)
for (let i = 7; i > 0; i --) {
s = intArr[i].toString(2)
while(s.length < 8) {
s = '0' + s
}
console.log(s)
}
- 首先我们的系统是低位优先系统(little_endian)
- 0 --> 标志位
- 011111111011 --> 指数位
- 1001{11}1010 --> 尾数
参考文献
- 《Javascript 权威指南》22.5章 类型化数组和 ArrayBuffer P678
- 彻底搞懂Javascript 浮点数
- 二进制转换工具
写在后面
- 祝大家多多发财