JS数值存储运算原理

开发 前端
JavaScript数值存储运算原理不知道大家了解不了解,今天我们一起来看一下!

前言

相信大家都看过这些曾经在社区比较火的文章:

  • 0.1 + 0.2 与0.3为什么不相等?
  • 为什么 3.0000000000000002 === 3表达式为true ?
  • 等...

造成这些问题的背后原因都是由于javaScript采用了 IEEE754 标准,全称 IEEE 二进制浮点数算术标准。所以说这个问题其实不止是会在javaScript中出现,而是「其他遵循 [IEEE 754]标准的语言也会出现这个问题」

并且自己在最近的工作中也遇到了这个问题,由于javaScript精度丢失而造成诡异问题!

javaScript车祸现场

上面三个例子在我们在控制台里面验证一遍,是不是瞬间觉得奇怪的知识又增加了?

javaScript这令人窒息的操作是不是让很多后端人员口吐芬芳了,甚至是很多前端人员都觉得明明都是送分题,却成了JS的送命题,工作中许多不经意间写出的bug,往往是由于JS的不按常理出牌。

说了这么多,我们也改变不了这一现状,那就尝试去理解它吧~

计算机运算

学过计算机相关同学都知道,我们的计算机底层元算采用的是二进制,而不是我们平常用的十进制!

二进制

「为什么计算机要采用二进制,而不是十进制?」

以下是在知乎上看到的回答,我觉得这个理解是比较到位的。

计算机本身的理论模型,和采用哪个数学上的进制完全无关,十进制也好,五进制也好,二进制也好,进制在数学上都是等价的,并没有哪个进制拥有其他进制无法实现的计算。

但计算机的实现是个工程问题,需要和真实的物理环境打交道,我们现在是用电路去实现我们的计算机模型,那就需要和物理电路打交道,需要考虑到信号的衰减延迟,电路器件的各种电气特性,什么电磁波干扰电流扰动,也就是会有失真的情况出现,而要最大程度避免衰减,失真对计算机这个完美世界造成破坏,同时要考虑电路的设计,制作成本,就需要最简单化的物理实现方案。

电子计算机确实是可以做成十进制的,就像题主说的像灯泡亮度分成十种亮度那样,但与此同时会出现很多的工程问题,比如对电子器件的精度和稳定性要求很高,电路设计的复杂性提升等等,到头来还不如就用二进制,在成本和质量上最划算。

事实上,不但十进制不行,十六进制、八进制、四进制也都比不上二进制。理论上已经证明效率最高的进制是e,离e最近的其实是三进制。但三进制不方便表示,不过也有人研究,前苏联就做过三进制计算机,国内也有,但并没有走出实验室。效率是一方面,实现成本又是一方面,最终大家还是觉得二进制实现起来最方便。

原码、反码、补码

「为运算方便,机器数有 3 种表示法,即原码、反码和补码」。

原码

原码是一种计算机中对数字的二进制定点表示法。「原码表示法在数值前面增加了一位符号位」。

反码

正数的反码和原码一样,

负数的反码就是在原码的基础上符号位保持不变,其他位取反。

补码

正数和 0 的补码就是该数字本身。

「负数的补码则是将其对应正数按位取反再加 1」

二进制转换

「正整数的转换方法」:除二取余,然后倒序排列,高位补零。

例如21的转换

商  余
21/2  10  1
10/2  5   0
5/2   2   1
2/2   1   0
1/2   0   1

21的二进制为10101,然后高位补0为00010101

「负整数的转换方法」:将对应的正整数转换成二进制后,对二进制取反,然后对结果再加一。

例如-21
先把21转换成二进制 00010101
逐位取反:11101010
再加1:11101011(补码)

「小数的转换方法」:对小数点以后的数乘以2,取整数部分,再取小数部分乘2,以此类推……直到小数部分为0或位数足够。取整部分按先后顺序排列即可。

例如123.4:
0.4*2=0.8 ——————-> 取0
0.8*2=1.6 ——————-> 取1
0.6*2=1.2 ——————-> 取1
0.2*2=0.4 ——————-> 取0
0.4*2=0.8 ——————-> 取0
………… 后面就是循环了
按顺序写出:0.4 = 0.01100110……(0110循环)
整数部分123的二进制是 1111011
则123.4的二进制表示为:1111011.011001100110……

发现了什么?十进制小数转二进制后大概率出现无限位数!但我们的javaScript采用了「IEEE754」 标准,全称 「IEEE 二进制浮点数算术标准」。

由于IEEE 754尾数位数限制,会将后面多余的位截掉。

javaScript 与 IEEE 754

“JavaScript 采用 IEEE 754 标准,数值存储为64位双精度格式,数值精度最多可以达到 53 个二进制位(1 个隐藏位与 52 个有效位)

图片

在这个标准下,我们会用1位存储 S(sign),0 表示正数,1 表示负数。用11位存储 E(exponent) + bias,对于11位来说,bias 的值是 2^(11-1) - 1,也就是 1023。用52 位存储 Fraction。

由于javaScript采用的是IEE754标准,所以在进制之间的转换过程中可能会导致精度丢失,这是造成javaScript运算翻车的罪魁祸首!

破案

0.1+0.2 与 0.3为什么不相等?

0.1.toString(2)
// '0.0001100110011001100110011001100110011001100110011001101'   // 57
// 按 IEEE754 格式  57 - 4 = 52可以精确存储
0.2.toString(2)
// '0.001100110011001100110011001100110011001100110011001101' // 56
// 按 IEEE754 格式  56 - 3 = 53  会丢弃最后一位数
0.3.toString(2)
// '0.010011001100110011001100110011001100110011001100110011'  // 56
// 按 IEEE754 格式  56 - 2 = 54  会丢弃最后两位数

/*总结:
存储0.1没有误差, 存储 0.2丢弃最后一位 1  存储0.3丢弃最后2位 11,
显然存储0.3丢弃的数值>存储0.2丢弃的数值
经分析 0.1 + 0.2   应该大于 0.3
*/
0.1 + 0.2 > 0.3  // true

为什么 3.0000000000000002 === 3表达式为true ?

手动将 3.0000_0000_0000_0002转换成二进制浮点数

整数部分为 11₂

小数部分0.0000_0000_0000_0002

0.0000_0000_0000_0002.toString(2)

'0.0000000000000000000000000000000000000000000000000000111001101001010110010100101111101100010001001101111'  

注意小数点后面正好有52个0

0.0000_0000_0000_0002.toString(2).length  // 105 105

将 3.0000000000000002 用 IEEE754 格式表示

  1. 符号S: 正数,0
  2. 指数位E:11 = 1.1 * 2^1 (二进制),E = 1023 + 1 = 1024 = 10000000000(二进制)
  3. 尾数位M:0.1.....0

所以该浮点数格式为: 0   1000_0000_000   1...000(一共52个0) 这个数正好是3。

如何解决精度问题?

目前有许多第三方库可以解决javaScript精度丢失的问题

math.js

math.js是JavaScript和Node.js的一个广泛的数学库。支持数字,大数,复数,分数,单位和矩阵等数据类型的运算。

官网:mathjs.org/ GitHub:github.com/josdejong/m…

0.1+0.2 ===0.3实现代码:

var math = require('mathjs')
console.log(math.add(0.1,0.2))//0.30000000000000004
console.log(math.format((math.add(math.bignumber(0.1),math.bignumber(0.2)))))//'0.3'

decimal.js

为 JavaScript 提供十进制类型的任意精度数值。

官网:mikemcl.github.io/decimal.js/

GitHub:github.com/MikeMcl/dec…

var Decimal = require('decimal.js')
x = new  Decimal(0.1)
y = 0.2
console.log(x.plus(y).toString())//'0.3'

bignumber.js

用于任意精度算术的JavaScript库。

官网:mikemcl.github.io/bignumber.j…

Github:github.com/MikeMcl/big…

var BigNumber = require("bignumber.js")
x = new BigNumber(0.1)
y = 0.2
console.log(x.plus(y).toString()) //'0.3'


责任编辑:华轩 来源: 前端南玖
相关推荐

2018-03-12 08:53:36

Ceph对象存储

2012-05-03 15:01:24

数值压缩

2022-02-09 08:11:50

架构

2022-06-08 07:34:02

持久化数据存储原理索引存储格式

2022-10-08 00:21:55

内存芯片RAM

2021-12-13 09:26:31

JS代码前端

2021-07-09 00:24:10

No.jsNode.js原理

2021-01-12 14:46:34

Kubernetes开发存储

2022-06-02 15:34:45

vmstorage监控

2021-06-11 18:27:10

LinuxLinux内核

2009-10-27 09:12:50

Visual Stud

2016-12-16 13:07:30

云存储运营混合云

2011-11-10 20:31:59

存储基础架构智慧的运算

2024-02-26 15:17:20

2022-10-27 16:07:24

littlefs存储结构

2017-06-12 11:09:32

存储AI阿里巴巴

2022-08-15 14:43:29

深度学习机器视觉机器人

2023-11-22 08:35:34

存储引擎bitcask

2018-06-13 08:53:39

HadoopHBase存储

2018-07-27 10:39:13

对象存储Git
点赞
收藏

51CTO技术栈公众号