JS数字精度

更新日期: 2020-02-05阅读: 2.6k标签: 数字

0.前言

最近在看计算机组成原理的浮点数部分,突然想起之前看过的一道快手面试题

为什么JS中0.1+0.2不等于0.3,应该如何解决?

这里我们可以借这道题来说一下JS的精度问题


1.JS数的储存

二进制和浮点数和定点数

首先计算机里面的数据肯定以二进制形式存储
对于同一段二进制码,不同的解读方式肯定有不同的意义
对于小数,我们有定点数和浮点数两种表示方法
目前计算机大多用浮点数,精度高,表示范围大

一个数以浮点数二进制码形式储存,我们从二进制浮点数码中能算出表达的二进制,然后二进制又可以得到相应的十进制,这就是他们的转化关系

复习浮点数

我们复习一下计组中对浮点数的介绍 这里以32位为例

如上图,32位二进制码中有三个部分,符号位,指数位,尾数位
浮点数计算公式:

从左往右看有三个部分,符号位指数位,尾数位
(1)(-1)^s表示符号位,当s=0,V为正数;当s=1,V为负数。
(2)2^E表示指数位。
(3)M表示有效数字,大于等于1,小于2。
这里有两个注意点,
1.M由于恒定为1.xxx,所以默认省略1
2.E不全为0或不全为1。这时E=E-127或者E=E-1023(64位)

我们按上面的规则算一下为什么图中0011111000100...00表示的0.15625
首先指数位置01111100表示的是124 则E=124-127=-3
然后我们看尾数,尾数位为1.01(加上了隐藏的1)
所以v=1.01*2的-3次方=0.00101
注意这个是二进制结果,转化为十进制的就是2(-3次方)+2(-5次方)=0.125+0.125*0.25=0.15625

JS中数值

JavaScript 内部,所有数字都是以64位浮点数形式储存,即使整数也是如此。所以,1与1.0是相同的,是同一个数。

我们看一下64位的JS数字是怎么储存的


JS中1和0.1的表示方法

http://www.binaryconvert.com/这个网站上我们可以找到一个数的二进制浮点数表示
我们手动按上面的方法算一下,并且验证看对不对

1的表示方法

1对应的二进制1.00000
那么1对应的浮点数应该长成这样
1.0* 2的0次方
指数为0 尾数为1.0 所以指数实际是1023 尾数是0000000000000...00

看来我们算的是对的

0.1的表示方法

0.1对应的二进制 0.000110011001100...(循环)
那么1对应的浮点数应该长成这样
1.10011001100....*2的-4次方
所以尾数位应该是10011001100....
指数应该是1019也就是01111111011

看来我们是对的


2.精度产生的原因

为什么精度会产生呢

首先0.1这种转化为二进制码是有误差的,尾数是一个不断循环的数,明显会有误差

其次,在浮点数加法运算里面,有对阶操作。対阶会损失一部分尾数,如果尾数后面都是0,没影响。但是如果是0.1转换成的二进制浮点数码的尾数,対阶的时候舍弃部分尾数明显也会造成误差。

如果一个大数和一个小数相加时,会产生很大的误差,因为対阶的时候尾数得截掉好多位

(1+0.1).toPrecision(20) //"1.1000000000000000888"
(100000000000+0.1).toPrecision(20)
"100000000000.10000610"

很明显误差大了


3.关于精度的额外知识点

0.1并不是0.1

上面的内容你如果理解了,你再看到0.1,你就会清楚,0.1并不是0.1,0.1的浮点数二进制码是有误差的,不可能算出0.1
我们看到的是浏览器帮我们做了处理的

不信,你可以试试

0.1.toPrecision(17)
// "0.10000000000000001"

JS安全数

面试喜欢考这个问题,啥叫安全数,就是在这个范围数值都有一正一反,一一对应
尾数一共52位,加上一个隐藏位,共53位
也就是JS能表示的最大整数是2的53次方,这个数是16位
但是

Math.pow(2, 53) === Math.pow(2, 53) + 1 // true

实际上2的53次方都不安全了,所以是2的53次方-1
在es6中 是Number.MAX_SAFE_INTEGER

toPrecision vs toFixed

数据处理时,这两个函数很容易混淆。它们的共同点是把数字转成字符串供展示使用。注意在计算的中间过程不要使用,只用于最终结果。

不同点就需要注意一下:

  • toPrecision是处理精度,精度是从左至右第一个不为0的数开始数起。
  • toFixed是小数点后指定位数取整,从小数点开始数起。

两者都能对多余数字做凑整处理,也有些人用toFixed来做四舍五入,但一定要知道它是有 Bug 的。

如:1.005.toFixed(2)返回的是1.00而不是1.01。

原因:1.005实际对应的数字是1.00499999999999989,在四舍五入时全部被舍去!

怎么解决这个问题呢,引入mathjs用它的round方法也可以,自己写一套字符串逻辑去处理也可以


4.解决方案

误差主要产生在进制转化和浮点数运算的対阶操作中
整数由于尾数后面全是0,同时转化为二进制数没有误差,所以

我们第一种方案就是全部转化为整数,计算完再转化为小数类似这种

/**
 * 精确加法
 */
function add(num1, num2) {
  const num1Digits = (num1.toString().split('.')[1] || '').length;
  const num2Digits = (num2.toString().split('.')[1] || '').length;
  const baseNum = Math.pow(10, Math.max(num1Digits, num2Digits));
  return (num1 * baseNum + num2 * baseNum) / baseNum;
} 

我们第二种方案就是用现成的库 mathjs之类的,原理就是不走浮点数那一套,转化成字符串,自己实现运算逻辑,从性能上说肯定比原生慢一点

当然如果仅仅是展示类型的,3.0000000001保留两位小数的这种还是tofixed(2)最快哦

链接: https://fly63.com/article/detial/7417

Js对数字类型的支持

由于 JavaScript 采用 IEEE 754 标准,数值存储为64位双精度格式,数值精度最多可以达到 53 个二进制位(1 个隐藏位与 52 个有效位)。如果数值的精度超过这个限度,第54位及后面的位就会被丢弃.

JS实现数字金额转大写金额

由于js的弱精度,在计算小数相加时总是会丢失精度,JS将金额数字转大写的方法封装,数字大写对照表;下面为大家整理些常用的实现方法。

JS判断值是否是数字

isNaN()的缺点就在于 null、空格以及空串会被按照0来处理;对于空数组和只有一个数值成员的数组,isNaN返回false。校验只要是数字(包含正负整数,0以及正负浮点数)就返回true

Js浮点型数字误差引发的问题

JavaScript 中浮点型精度的误差,是非常基础但是却又经常不被重视的问题。文中分享的方案,足以覆盖项目中的所有情况,但如果用在其它地方或项目中,在一些极端情况下可能会有问题。

vue限制文本框输入数字的正确姿势

最近遇到一个需求,需要限制文本框输入数字,而number类型的输入框有箭头,个人不是很喜欢,因此想要寻求其它途径实现。本想通过网上找个现成的插件,然而百度,谷歌一番都没有找到满意的答案,至于随手一搜出来的方案或多或少都有点缺陷。因此自己动手,丰衣足食。

Js中的无穷数(Infinity)

Infinity(无穷大)在 JS 中是一个特殊的数字,它的特性是:它比任何有限的数字都大,如果不知道 Infinity, 我们在一些运算操作遇到时,就会觉得很有意思。现在我们来看看 JS 中的Infinity 属性,了解用例并解决一些常见的陷阱。

Js将负数转换为正数?

这是一种通用方法,我们首先检查数字是已经是正数还是负数,如果数字是负数,那么我们将数字乘以-1以使其为正数。使用Math.abs()方法将负数转换为正数。

Js如何从字符串中提取数字?

如果想要将一个字符串中的数字给提取出来,这要怎么做? 在JavaScript中可以使用match()方法将字符串中的数字提取到数字数组中。此方法将正则表达式作为参数,并从字符串中提取数字。使用match()方法提取数字分两种情况:

javascript如何判断值是否是数字?

javascript如何判断一个值是否是数字?下面本篇文章就来给大家介绍一下使用javascript判断一个值是否是数字的方法,sNaN() 函数用于检查其参数是否是非数字值。如果 值x 是特殊的非数字值 NaN

javascript如何判断两个数是否整除?

JavaScript判断两个整数(a,b,a>b)的整除时,可以用Number.isInteger(a/b)或者a%b==0来进行判断,但是如果其中涉及到小数就会导致计算过程中精度丢失。

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!