0.1 + 0.2不等于0.3?为什么JavaScript有这种“骚”操作_js 为什么x=0.1 可以得到0.1-程序员宅基地

技术标签: js  

写在前面

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

JavaScript中计算的翻车现场

言归正传 书接上文,先来一个简单(landajie)的?,展示一下js计算的常规操作

 

这种送分题,js却送了命。令人窒息的操作。这个例子很常见,我们不是为了关注这个例子本身,我们需要明白的是为什么会出现这样的结果?哪一步出了问题?还有那些计算可能会出现这样的问题?怎么解决?

 

JavaScript是如何表示数字的?

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

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

 

 

图片文字说明

 

  • 第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位)

简单验证一下

 

 

运算时发生了什么?

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

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

1.进制转换

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

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不要去纠结哪种语言了,文章重点不是语言,谢谢


作者:Gladyu
链接:https://juejin.im/post/5b90e00e6fb9a05cf9080dff
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_34629352/article/details/100020922

智能推荐

eclipse交叉编译linux内核,【已解决】Ubuntu下Eclipse中交叉编译Uboot出错:make[1]: arm-xscale-linux-gnueabi-gcc: Command no...-程序员宅基地

文章浏览阅读406次。【问题】折腾:期间,编译出错:22:21:49 **** Build of configuration Default for project HART-IP ****make allmake[1]: arm-xscale-linux-gnueabi-gcc: Command not foundmake[1]: Entering directory `/home/crifan/develop/ec..._uboot arm-linux -gcc command not found

原码、反码、补码和移码其实很简单_0111-1011-程序员宅基地

文章浏览阅读3k次。计算机组成原理,看到书中关于原码、反码、补码和移码的定义如下(n是机器字长):原码:反码:补码:移码:看完这些定义以后,我的脑袋瞬间膨胀到原来的二倍!这样变态的公式不管你记不记得住,反正我是记不住!其实没必要弄得这么麻烦,它们完全可_0111-1011

Linux创建后台进程_linix后台进程创建-程序员宅基地

文章浏览阅读1k次。void setdaemon(const char * pid_file){ pid_t pid; if(!pid_file) { return; } if((pid = fork()) < 0) { exit(-1); } if(pid) { exit(0); } setsid(); if ((pid = fork()) < 0) { ..._linix后台进程创建

让react用起来更得心应手——(react-router原理简析)_hashchange和popstate无法触发-程序员宅基地

文章浏览阅读1.1k次,点赞3次,收藏10次。让react用起来更得心应手系列文章:让react用起来更得心应手——(react基础简析) 让react用起来更得心应手——(react-router原理简析) 让react用起来更得心应手——(react-redux原理简析)前端路由和后台路由在刚入行的时候一直明白什么单页面应用是什么,说白了就是混淆了前台路由和后台路由,现在来缕缕它们:前台路由:页面的显示由前台js控制,在..._hashchange和popstate无法触发

Git应用笔记整理(全)_git 记笔记-程序员宅基地

文章浏览阅读913次,点赞2次,收藏4次。1. Git 配置1.1. git config  git config 语法:git config [–环境参数] key value   有三个环境参数:–global 全局配置,写到~/.gitconfig中,即用户路径C:\Users\li.liu下的.gitconfig文件。–local 工作目录配置,即所在仓库的配置,写到当前仓库下的.git/config文件中。..._git 记笔记

matlab批量读取文件夹里面的文件名,并且调整图片大小,再按照原名称输出_matlab导出图片且和原图片一样的文件名-程序员宅基地

文章浏览阅读1.8k次。举个例子,我要批量修改某文件夹里面的图片大小,我不想用顺序命名,之后我还想原名输出fileFolder=fullfile('E:\caffe\SegNet_ip\CamVid\test');%读取图片路径dirOutput=dir(fullfile(fileFolder,'*.jpg'));%读取文件夹里面文件OtpDir = 'E:\test';%输出路径fileNames = {di_matlab导出图片且和原图片一样的文件名

随便推点

【多线程与高并发3】常用锁实例_多线程枷锁的案例-程序员宅基地

文章浏览阅读328次。各式锁的实际应用乐观锁 cas(要改的对象,期望的值,要给的值)无锁操作,其实是一个乐观锁…cas本身可以看成是一个锁automic : 一种使用 cas 实现的原子性操作原子操作的简单方法:函数效果备注AtomicInteger a = new AtomicInteger(0);int a = 0;创建对象a并且赋初值为0;a.incrementAndGet( );i++;对原值+1后返回;a.getAndIncrement( );_多线程枷锁的案例

强化学习笔记(5)之时序差分法_td error-程序员宅基地

文章浏览阅读2.2k次,点赞4次,收藏18次。强化学习笔记(5):时序差分法求值函数标签(空格分隔): 未分类文章目录强化学习笔记(5):时序差分法求值函数时序差分法与动态规划法和蒙特卡洛法的区别TD方法的反演同策略的时序差分法:SARSASARSA的收敛性SARSA($\lambda$)时序差分法与动态规划法和蒙特卡洛法的区别动态规划法(DP): 需要状态模型,即状态转移矩阵Pss′aP_{ss&#x27;}^aPss′a..._td error

intellij 下编译单个(没有main函数的)java文件_没有main idea编译-程序员宅基地

文章浏览阅读1.8w次,点赞7次,收藏9次。图中这个按钮可以实现对单个没有main函数的java文件进行编译这个问题真是大写的草泥马,之前google了无数文章,也许是觉悟太低,就是没有找到问题解决方法,在今天偶的看到一篇叫“关于Intellij IDEA菜单项中Compile、Make和Build的区别”的文章后才无意间解决了这个问题,踏破铁鞋无觅处,得来全不费功夫啊啊啊啊!!_没有main idea编译

创建全景图-程序员宅基地

文章浏览阅读323次。创建全景图在同一位置(即图像的照相机位置相同)拍摄的两幅或者多幅图像是单应性相关的,使用该约束将很多图像缝补起来,拼成一个大的图像来,创建全景图像。目标:将数张有重叠部分的图像通过特征点检测,匹配,图像变换拼成一幅无缝的全景图或高分辨率图像。如何创建全景图像?1、 提供一组图像集,实现特征匹配(相邻图像之间要有重复区域)2、通过匹配特征计算图像之间的变换结构3、利用图像变换结构,实现图像映射4、 针对叠加后的图像,采用APAP之类的算法,对齐特征点5、 通过图割方法,自动选取拼接缝实现图像_创建全景图

vector erase() and clear() in C++ -- vector的函数erase()和clear()_vector.earse clear-程序员宅基地

文章浏览阅读1.3k次。Vectors are same as dynamic arrays with the ability to resize itself automatically when an element is inserted or deleted, with their storage being handled automatically by the container. vector和动态数组一..._vector.earse clear

Access Token访问令牌的操作_xilinx访问令牌-程序员宅基地

文章浏览阅读4.7k次。access token 访问令牌的概念Windows操作系统安全性的一个概念。一个访问令牌包含了此登陆会话的安全信息。当用 用户权利指派户登陆时,系统创建一个访问令牌,然后以该用户身份运行的的所以进程都拥有该令牌的一个拷贝。该令牌唯一表示该用户、用户的组和用户的特权。系统使用令牌控制用户可以访问哪些安全对象,并控制用户执行相关系统操作的能力。有两种令牌:主令牌和模拟的令牌。主令牌是与进程相关的;模拟的令牌是与模拟令牌的线程相关的。  进程拥有某种令牌就表示它拥有某种特权。什么_xilinx访问令牌