同步发表在我的博客:jmingzi

当你学习一个知识点没有方向时,可以尝试以解决问题的角度来理解它。

例如这个知识点我们可以从以下问题开始:

  • 你看的到 1 真的是整数 1 吗?
  • 为什么0.1 + 0.2 得到的是 0.30000000000000004 而不是 0.30000000000000004999 ?
  • 为什么最大安全数是 2^53 - 1 ?
  • 如何避免精度问题?
  • 构造函数 Number 的一些静态属性

问题一

我们需要知道 js 中没有真正的整数,我们看到的数值都是 v8 引擎省略精度后的结果。在 ecma-262 规范中并没有说明该如何省略精度,所以如果换个解析引擎,可能又是另外一种结果了。

1..toPrecision(4) === '1.000'

我们可以用 toPrecision 来获取数值精度的字符串表示,所以对 js 中的数你得有 “横看成岭侧成峰” 的感觉。

问题二

由问题一我们知道精度是引擎处理的结果,那么这个问题也是同样的道理,0.1 + 0.2 如果放大精度来看,得到的当然是

(0.1 + 0.2)..toPrecision(21) === '0.300000000000000044409' 

猜测:小数部分的精度默认是非 0 的 17 位,如果末尾是 0 则继续省略。接下来,我们从验证问题三开始从原理入手。

问题三

javascript 的浮点数采用的是 IEEE 754 双精度 64 位表示,IEEE 754 规定了四种浮点数的表示方式,双精度 64 位是其中的一种。

64 位二进制组成:

  • 第 1 位符号位,用 S 表示,0 代表正数,1 代表负数
  • 第 2 - 12 位指数位,用 E 表示,即 2^11 = 2048,曲值范围是 0 ~ 2047,由于是双精度,所以指数部分有正数也有负数,取中值 1023 分隔,即指数值为 E - 1023
  • 第 13 - 64 位尾数位,用 M 表示

例如数字 1 用 二进制表示也是 “1”,如果我们填充 64 位二进制,表示应该是怎样的呢?

  • 符号 S = 0
  • 指数 E - 1023 = 0,E = 1023
  • 尾数 M = 52

那么实际我们看到的二进制存储应该是这样的:

0 01111111111 0000...0000

用在线转换工具转换后如下图,可以验证结果的正确性:

http://www.binaryconvert.com/result_double.html

我们再用 0.1 + 0.2 验证下:

0.1.toString(2)
// 0.00011001100...11010

0.1 的二进制 64 位转成科学记数法表示:1.10011001100...*10^-4,也就是说

  • 符号 S = 0
  • 指数 E = 1023 - 4 = 1019
  • 尾数 M = 10011001100...11010 我们可以继续用在线转换的网址验证下结果:

我们可以发现,其实 0.1 的尾数是 1100 不断循环的,但是我们看到的最后 4 位是 1010 这是由于尾数只能保存 52 位,多余的部分会被舍弃,舍弃规则是 IEEE 754 规范所定义的:

  • 舍入到最接近:舍入到最接近,在一样接近的情况下偶数优先(Ties To Even,这是默认的舍入方式),会将结果舍入为最接近且可以表示的值,但是当存在两个数一样接近的时候,则取其中的偶数(在二进制中式以0结尾的)
  • 朝+∞方向舍入
  • 朝-∞方向舍入
  • 朝 0 方向舍入

另外,规范还约定,由于二进制的科学记数法永远是 1.几 开头,所以将 1 省略,这样尾数就有 53 位来表示。所以 0.1 的二进制尾数部分:11001100...11001 这样 53 位,最后一位 1 向偶数舍入即进 1,得到 1010,这样得到的数其实是比真实的 0.1 要大的。

同理,我们查看 0.2 的表示:0.00110011001100...11010,可以得到尾数部分其实是一样的,仅仅是指数少了 1 位。

我们再来看看问题三,为什么最大安全整数是 2^53 - 1?我们可以反过来验证为什么 2^53 已经不再“安全”了。

由问题一我们知道正指数最大为 2047 - 1023 = 1024,为什么不是 2^1024 呢?由上我们知道尾数其实是可以有 53 位表示的(省略的 1 位),即

// Math.pow(2, 53).toString(2)
2^53 用二进制表示:1000...000,1 个 1,53 个 0
// 此时的尾数最多为 53 位,第 54 位 0 会被舍去 2^53-1 用二进制表示:1111...111,53 个 1
2^53+1 用二进制表示:1000...0001,1 个 1,52 个 0,1 个 1

其中,2^53 + 1 由于尾数最多为 53 位,所以必须舍掉第 54 位 1,根据舍入规则,向偶数舍入,所以舍掉第 54 位 1,不进 1。于是得到

2^53 === 2^53+1

也就是说从 2^53 开始就不能唯一表示一个数了,所以才说 2^53-1 是最大的安全数。

问题四

前端避免精度的场景就是展示某个价格,例如下面的公式:

展示价格 = 商品价格 * 数量 + 总价 * 服务费比例 - 优惠券价格

我们常用的方法有 Number.toFixed()、Number.parseFloat()、Math.round(),它们的区别是什么,弄清楚后才能知道如何使用它们。

Number.toFixed(digits) 返回指定位数的字符串表示,会进行四舍五入。例如

1.005.toFixed(2) // 1.00

很显然不符合我们需求,为什么会这样呢?因为 1.005 这个数在 64 位二进制存储时是不能完全表示这个数的,我们放大精度看看

1.005.toPrecision(17)
// 1.0049999999999999

所以四舍五入的时候就将 499...99 舍掉了。

还记得问题三么?0.300000000000000044409 ,猜测的是是小数位 17 位,超过 17 位的部分会被四舍五入。因为做精度运算时都会做四舍五入:

1.005.toPrecision(16)
// 1.005000000000000 1.005.toPrecision(17)
// 1.0049999999999999 1.005.toPrecision(18)
// 1.00499999999999989

所以使用 toFixed 去做展示运算是不可靠的。

Number.parseFloat === parseFloat ,将字符串转换为浮点数表示,很显然转换后显示出来也会有精度问题,因为精确到哪一位呢?

我上面猜测说是:“小数部分的精度默认是非 0 的 17 位”,这是不准确的,例如:

1.005 * 100
// 100.49999999999999
100.49999999999999.toPrecision(20)
// 100.49999999999998579
'49999999999999'.length
// 14 并不是 17

所以 parseFloat 后的结果和我们直接写出来看到的数字是一样的,并不能够使用它直接参与计算。

Math.round() 返回一个数字四舍五入后最接近的整数,所以我们一般将浮点数放大为精度位的整数后再使用 Math.round() 得到四舍五入后的整数,再缩小精度位。

例如对于价格类,精度为 2,我们可以先乘 100 做运算后再除 100,此时得到的很可能是个精度位很长的数,我们只需要在展示时乘 100,Math.round 后再除 100 即可。

所以结论就是对于浮点数的计算,先放大,再做计算,计算完成后需要展示精度,可以使用 Math.round 四舍五入后,再缩小即可。

对于不需要做计算的浮点数直接展示,我们可以先 toPrecision 放大精度,再使用 toFixed() 到指定的精度。前提是放大的精度足够大,最好是 17 。

function round(num, precision) {
const base = Math.pow(10, precision)
return Math.round((num.toPrecision(17) * base).toFixed(1)) / base
}
round(1.005, 2)
// 1.01

问题五

Number 构造函数拥有的静态属性如下(负方向已忽略):

// 最大能表示的值,无限接近于 2^1024
Number.MAX_VALUE
// 最大安全数 2^53 - 1
Number.MAX_SAFE_INTEGER
// 正无穷大 2^1024
Number.POSITIVE_INFINITY === Infinity
// 非数值
Number.NaN 等同于 NaN

关于 Infinity 和 NaN 需要注意的是

  • Infinity === Infinity
  • NaN !== NaN
  • Infinity / Infinity = NaN
  • 0 * Infinity = NaN
  • 任何数 和 NaN 运算结果都是 NaN
  • 任何数 / Infinity = 0
  • Infinity * Infinity = Infinity

关注我的公众号,获取更多干货~

Js 与浮点数的更多相关文章

  1. js,java,浮点数运算错误及应对方法

    js,java浮点数运算错误及应对方法 一,浮点数为什么会有运算错误 IEEE 754 标准规定了计算机程序设计环境中的二进制和十进制的浮点数自述的交换.算术格式以及方法. 现有存储介质都是2进制.2 ...

  2. js运算浮点数

    在js中做小数:9.3+0.3会发现,得到的结果并不是9.6,而是9.600000000000001.这是为什么? Javascript采用了IEEE-745浮点数表示法,这是一种二进制表示法,可以精 ...

  3. JS/PHP 浮点数精确运算

    php浮点数精确运算 bc是Binary Calculator的缩写.bc*函数的参数都是操作数加上一个可选的 [int scale],比如string bcadd(string $left_oper ...

  4. js 计算浮点数

    JS的浮点计算 最近遇到了数值计算的时候,计算结果出现了类似于199.9999999999999999999的情况,但是被用来计算的两个数值都只是两位数. 就像这样      --------> ...

  5. js处理浮点数计算误差

    众所周知,浮点计算会产生舍入误差的问题,比如,0.1+0.2,结果应该是0.3,但是计算的结果并不是如此,而是0.30000000000000004,这是使用基于IEEE754数值的浮点计算的通病,j ...

  6. js如何计算浮点数

    js中浮点型是如何运算的呢? 例如:var a=0.69; 我想得到6.9 直接这样写 var c=a*10; alert(c);   得到结果是:6.8999999999999995 到网上一搜,有 ...

  7. js浮点数精度丢失问题及如何解决js中浮点数计算不精准

    js中进行数字计算时候,会出现精度误差的问题.先来看一个实例: console.log(0.1+0.2===0.3);//false console.log(0.1+0.1===0.2);//true ...

  8. JS里浮点数的运算

    //浮点数加法运算 function FloatAdd(arg1,arg2){ var r1,r2,m; try{r1=arg1.toString().split(".")[1]. ...

  9. JS中浮点数精度误差解决

    问题出现 0.1 + 0.2 = 0.30000000000000004 问题分析 对于浮点数的四则运算,几乎所有的编程语言都会有类似精度误差的问题,只不过在 C++/C#/Java 这些语言中已经封 ...

随机推荐

  1. 领扣(LeetCode)最大连续1的个数 个人题解

    给定一个二进制数组, 计算其中最大连续1的个数. 示例 1: 输入: [1,1,0,1,1,1] 输出: 3 解释: 开头的两位和最后的三位都是连续1,所以最大连续1的个数是 3. 注意: 输入的数组 ...

  2. 关于Java 值传递深度分析

    首先说观点:java只有值传递没有引用传递 然后再来看看值传递与引用传递两者的定义 值传递(pass by value)是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改, ...

  3. centos7 防火墙屏蔽IP

    1.屏蔽指定IP:124.115.0.199 iptables -I INPUT -s 124.115.0.199 -j DROP 2.屏蔽IP段: iptables -I INPUT -s 61.3 ...

  4. [FPGA]Verilog实现寄存器LS374

    目录 想说的话... 正文 IC介绍 电路连接图 功能表 逻辑图 实验原理 单元实现_D触发器 整体实现(完整代码) 想说的话... 不久前正式开通了博客,以后有空了会尽量把自己学习过程中的心得或者感 ...

  5. PostGIS 导入SHP文件并与ArcGIS连接

    运行环境: ArcGIS10.4 PostGreSql9.4 PostGIS2.2(需勾选空间数据库,否则需要重新安装) 实现步骤: 方法一: 1.打开pgAdminIII,数据库节点上右键,新建数据 ...

  6. Spring Cloud Alibaba(四)实现Dubbo服务消费

    本项目演示如何使用 Spring Cloud Alibaba 完成 Dubbo 的RPC调用. Spring Cloud与Dubbo Spring Cloud是一套完整的微服务架构方案 Dubbo是国 ...

  7. sku二维数组里的数组从头到尾叠加组合

    今天工作之余与同事聊天,要是实现一个sku描述里的字段组合的问题.并且实现了请吃饭.哈哈.一顿饭,我和另一位同事积极杠杆的.后来实现了出来. let skuList = [ ['黑色', '白色',' ...

  8. 【记录】洛谷P1739-表达式括号匹配AC记

    题面请查看:https://www.luogu.org/problem/P1739 思路: 见到括号就搜索,搜到与它配对的括号为止,搜不到就输出NO 代码: #include <bits/std ...

  9. Virtualbox修改虚拟机分配内存的大小

    起因:因为虚拟机刚开始分配的内存太小,导致太卡, 解决方法:修改虚拟机分配内存的大小 方法一:必须在关闭ubuntu的前提下进行,否则无法修改 点击设置 系统选项 主板中的内存大小 之后开启即可 方法 ...

  10. https的安装(基于阿里云)

    背景介绍:首先我的服务器在是阿里云的云服务器,web服务器使用的是nginx 进入到阿里云的ssl证书的管理界面,按需选择套餐后进行申请,申请完成后进行补全操作,最后是变成如下界面点击--下载进行证书 ...