0.1 + 0.2不等于0.3?为何JavaScript有这种“骚”操做?

写在前面

随着消费观念的改变,线上消费已经成为大众生活中不可或缺的一部分。在保证消费安全和用户隐私的同时,精准度也是必不可少的一环。试想一下,用户在一款产品上消费,结算金额出错,用户会怎么想?(数体教 or WTF?),妥妥的差评了吧。 这样不要说用户粘性了,留存都是问题。当Boss得知用户的遭遇后,估计贡献代码的同志会成为前员工或者你们口中的已故员工某某某。做为一个优秀(laji)的程序员,很久以前就遇到过精确计算的问题,可是偷懒并无整理出来,直到最近有人问我相关问题,忽然以为有必要写写我对js精确计算的理解html

JavaScript中计算的翻车现场

言归正传 书接上文,先来一个简单(landajie)的🌰,展现一下js计算的常规操做 git

这种送分题,js却送了命。使人窒息的操做。这个例子很常见,咱们不是为了关注这个例子自己,咱们须要明白的是 为何会出现这样的结果?哪一步出了问题?还有那些计算可能会出现这样的问题?怎么解决?

JavaScript是如何表示数字的?

JavaScript使用Number类型表示数字(整数和浮点数),遵循 IEEE 754 标准 经过64位来表示一个数字程序员

经过图片具体看一下数字在内存中的表示github

图片文字说明

  • 第0位:符号位,0表示正数,1表示负数(s)
  • 第1位到第11位:储存指数部分(e)
  • 第12位到第63位:储存小数部分(即有效数字)f

既然说到这里,再给你们科普一个小知识点:js最大安全数是 Number.MAX_SAFE_INTEGER == Math.pow(2,53) - 1, 而不是Math.pow(2,52) - 1, why?尾数部分不是只有52位吗?安全

这是由于二进制表示有效数字老是1.xx…xx的形式,尾数部分f在规约形式下第一位默认为1(省略不写,xx..xx为尾数部分f,最长52位)。所以,JavaScript提供的有效数字最长为53个二进制位(64位浮点的后52位+被省略的1位)bash

简单验证一下 函数

运算时发生了什么?

首先,计算机没法直接对十进制的数字进行运算,这是硬件物理特性已经决定的。这样运算就分红了两个部分:先按照IEEE 754转成相应的二进制,而后对阶运算网站

按照这个思路分析一下0.1 + 0.2的运算过程ui

1.进制转换

0.1和0.2转换成二进制后会无限循环spa

0.1 -> 0.0001100110011001...(无限循环)
0.2 -> 0.0011001100110011...(无限循环)
复制代码

可是因为IEEE 754尾数位数限制,须要将后面多余的位截掉(本文借助这个网站直观展现浮点数在内存中的二进制表示)

0.1

0.2

这样在进制之间的转换中精度已经损失

这里还有一个小知识点

那为何 x=0.1 能获得 0.1?

这是由于这个 0.1 并非真正的0.1。这不是废话吗?别急,听我解释

标准中规定尾数f的固定长度是52位,再加上省略的一位,这53位是JS精度范围。它最大能够表示2^53(9007199254740992), 长度是 16,因此可使用 toPrecision(16) 来作精度运算,超过的精度会自动作凑整处理

0.10000000000000000555.toPrecision(16)
// 返回 0.1000000000000000,去掉末尾的零后正好为 0.1

// 但来一个更高的精度:
0.1.toPrecision(21) = 0.100000000000000005551
复制代码

这个就是为何0.1能够等于0.1的缘由。好的,继续

2.对阶运算

因为指数位数不相同,运算时须要对阶运算 这部分也可能产生精度损失

按照上面两步运算(包括两步的精度损失),最后的结果是

0.0100110011001100110011001100110011001100110011001100 
复制代码

结果转换成十进制以后就是0.30000000000000004,这样就有了前面的“秀”操做:0.1 + 0.2 != 0.3

因此:

精度损失可能出如今进制转化和对阶运算过程当中

精度损失可能出如今进制转化和对阶运算过程当中

精度损失可能出如今进制转化和对阶运算过程当中

只要在这两步中产生了精度损失,计算结果就会出现误差

怎么解决精度问题?

1.将数字转成整数

这是最容易想到的方法,也相对简单

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;
}
复制代码

可是这种方法对大数支持的依然很差

2.三方库

这个是比较全面的作法,推荐2个我平时接触到的库

1).Math.js

专门为 JavaScript 和 Node.js 提供的一个普遍的数学库。支持数字,大数字(超出安全数的数字),复数,分数,单位和矩阵。 功能强大,易于使用。

官网:mathjs.org/

GitHub:github.com/josdejong/m…

2).big.js

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

GitHub:github.com/MikeMcl/big…

3)若干,不一一列举了

这几个类库都很牛逼,能够应对各类各样的需求,不过不少时候,一个函数能解决的问题不须要引用一个类库来解决。

以上就是我对js精准计算的理解,但愿能够帮到你们

转载必须标明出处,谢谢。文章有疏漏浅薄之处,请各位大神斧正

说明

看了评论不少人说:其余遵循 IEEE 754 标准的语言也有这个问题,我知道其余的语言也有,可是这篇文章是以js为切入点去分析的,so不要去纠结哪一种语言了,文章重点不是语言,谢谢

看了评论不少人说:其余遵循 IEEE 754 标准的语言也有这个问题,我知道其余的语言也有,可是这篇文章是以js为切入点去分析的,so不要去纠结哪一种语言了,文章重点不是语言,谢谢

看了评论不少人说:其余遵循 IEEE 754 标准的语言也有这个问题,我知道其余的语言也有,可是这篇文章是以js为切入点去分析的,so不要去纠结哪一种语言了,文章重点不是语言,谢谢

相关文章
相关标签/搜索