“巨坑”的toFixed

在分享前,大家可以分享一下让你感到困惑的一些 api/运算符等。

  1. 双等号
  1. String.prototype.length / String.prototype.slice 等码元计数的

String 类型的 length 数据属性表示字符串的 UTF-16 码元长度。很多时候我们需要以码点来计数进行操作,大家感兴趣可以看我上次的分享《前端漫谈》

  1. Number.prototype.toFixed

我们今天就重点来研究 toFixed 这个api。

遇到的一些问题

  1. 他们的执行结果如何呢?

1.55.toFixed(1)

2.45.toFixed(1)

2.55.toFixed(1)

3.55.toFixed(1)

  1. 小数部分一样为啥 toFixed 的结果不一样?

1.55.toFixed(1) 和 2.55.toFixed(1)

  1. 0.1 + 0.2 !== 0.3 的真实原因

  2. 0.2 + 0.3 === 0.5 为什么是对的呢?

  3. 是不是所有小数都有会被截断存储呢?

  4. 小数截断时四舍五入合理吗🤔️?

今天就从小数的存储、运算、展示三个方面来分析下。

小数的存储

回顾一下整数和小数如何转二进制

整数转二进制

除二取余并倒排

小数转二进制

乘二取整并顺排

二进制转十进制

案例1 0.1 和 0.2 的二进制存储/表示

arduino 复制代码
0.2 二进制存的啥?
0.2.toString(2)
'0.001100110011001100110011001100110011001100110011001101'
'0.0011001100110011001100110011001100110011001100110011001100'
0.2 在二进制里面没法真实表示,会被转成无限循环小数 '0011'

0.2 截断过程
0.001100110011001100110011001100110011001100110011001100110011...
我们看红色标记的位本来是 0 后面的都应该截断,因为下一位是 1 ,所以这里截断的时候进位了变成了
0.001100110011001100110011001100110011001100110011001101
所以实际存储的值会比 0.2 大。
0.2.toPrecision(50)
'0.20000000000000001110223024625156540423631668090820'

0.1 二进制存的啥?
0.1.toString(2)
'0.0001100110011001100110011001100110011001100110011001101'
'0.000110011001100110011001100110011001100110011001100110011'
重复上面的分析过程,得到实际存储的值会比 0.1 大。

案例2 小数部分一样为啥 toFixed 的结果不一样?🤔️🤔️

是不是所有小数都有会被截断存储呢?

能用二进制表示的场景

实际上 1/2^n 的小数是可以被有限二进制表示和存储的。

This content is only supported in a Feishu Docs

小数的运算

案例1 0.1 + 0.2 !== 0.3

vbscript 复制代码
0.3 二进制存的啥?
0.3.toString(2)
'0.010011001100110011001100110011001100110011001100110011'
0.010011001100110011001100110011001100110011001100110011
0.0100110011001100110011001100110011001100110011001100110011
重复上面的分析过程,得到实际存储的值会比 0.3 小。

0.1 + 0.2 > 0.3

通过分析,我们知道 0.3 实际存储的结果要比自身小。结合第一节中的案例1 0.1 和 0.2 的二进制存储,小数的存储中 0.2 和 0.1 实际存储的结果都比自身大,所以 0.1 + 0.2 执行结果肯定大于 0.3。

那 0.2 + 0.3 === 0.5 也是碰巧成立的。

小数的展示

浏览器一直在猜我们要做什么。0.2 实际存储是 0.20000000000000001110 ,但真正展示时 0.2。

0.3 - 0.2 为 0.09999999999999998,精度损失被放大了,浏览器也不敢截断了。

存储的0.3比真实的0.3小,存储的0.2比真实的0.2大,所以误差放大了。

回归本质

Number 编码(MDN)

JavaScript 的 Number 类型是一个双精度 64 位二进制格式 IEEE 754 值,类似于 Java 或者 C# 中的 double。这意味着它可以表示小数值,但是存储的数字的大小和精度有一些限制。简而言之,IEEE 754 双精度浮点数使用 64 位来表示 3 个部分:

  • 1 位用于表示符号(sign) (正数或者负数)
  • 11 位用于表示指数(exponent) (-1022 到 1023)
  • 52 位用于表示尾数(mantissa) (表示 0 和 1 之间的数值)

尾数(也称为有效数)是表示实际值(有效数字)的数值部分。指数是尾数应乘以的 2 的幂次。将其视为科学计数法:

尾数使用 52 比特存储,在二进制小数中解释为 1.... 之后的数字。因此,尾数的精度是 2(-52)(可以通过 Number.EPSILON 获得),或者十进制数小数点后大约 15 到 17 位;超过这个精度的算术会受到舍入的影响。

  • 一个数值可以容纳的最大值是 2(1024) 1(指数为 1023,尾数为基于二进制的 0.1111...),可以通过 Number.MAX_VALUE 获得。超过这个值的数会被替换为特殊的数值常量 Infinity

  • 只有在 -2(53) 1 到 2(53) 1 范围内(闭区间)的整数才能在不丢失精度的情况下被表示(可通过 Number.MIN_SAFE_INTEGERNumber.MAX_SAFE_INTEGER 获得),因为尾数只能容纳 53 位(包括前导 1)。

Number.EPSILON

Number.EPSILON === Math.pow(2, -52)

只要误差比 Number.EPSILON 小就认为是合理的误差阈值。Number.EPSILON === Math.pow(2, -52)

Number.MAX_SAFE_INTEGER

Number.MAX_SAFE_INTEGER === Math.pow(2, 53) - 1

表示在 JavaScript 中最大的安全整数(2^53 -- 1)

IEEE754

该标准的全称为IEEE二进制 浮点 数算术标准( ANSI /IEEE Std 754-1985)

baseconvert.com/ieee-754-fl...

www.h-schmidt.net/FloatConver...

IEEE 754 半精度浮点数:16 位 符号 1 位,指数 5 位,尾数 10 位
IEEE 754 单精度浮点数:32 位 符号 1 位,指数 8 位,尾数 23 位
IEEE 754 双精度浮点数:64 位 符号 1 位,指数 11 位,尾数 52 位

解答案例2 小数一样为啥 toFixed 的结果不一样?🤔️🤔️

1.55 和 3.55 虽然他们小数部分完全一样,但是在 ieee754 中存储时,小数截断的位置不同导致,1.55 进位而3.55舍去了。

数值修约

数值修约是指在运算数字前,按照一定的规则确定一致的位数,然后舍去某些数字后面多余尾数的过程。

现在广泛使用的数值修约规则,主要有四舍五入五舍六入四舍六入五留双、无条件舍去规则。

四舍五入真的合理吗🤔️?

无条件修约

无条件修约分为下取整、上取整、截尾取整(无条件舍去)、无条件进位,分述如下:

"四舍五入"方法看似合理,其实不然。 舍的是" 1、2、3、4",入的却是"5、6、7、8、9",入的概率大于舍的概率。

0.0

0.1

0.2

银行家舍入法是由IEEE 754标准规定的浮点数取整算法,大部分的编程软件都使用的是这种方法。 所谓银行家舍入法,其实质是一种四舍六入五取偶(又称四舍六入五留双)法。

"四舍六入五成双",也即"4舍6入5凑偶",这里"四"是指≤4时舍去,"六"是指≥6时进上。"五"指的是根据5后面的数字来定,当5后有数时,舍5入1;当5后无有效数字时,需要分两种情况来讲:5前为奇数,舍5入1;5前为偶数,舍5不进(0是偶数)。

银行家舍入法来源,起初是为了解决银行结算时避免误差被放大而提出的。

就想要四舍五入怎么办?

一个的任意精度十进制类型JavaScript库: decimal.js

csharp 复制代码
let a = 0.2, b = 0.1;
let c = new Decimal(a).add(new Decimal(b));
c // 0.3

四舍五入的效果

Math.round() 函数返回一个数字四舍五入后最接近的整数。

转成字符串,先放大后缩小。

还是直接上 lodash 吧!!!

_.round(2.55, 1)

IEEE-854

另外一个标准是IEEE 854,"与基数无关的浮点数"的"IEEE 854-1987标准",有规定基数为2跟10的状况。但没有规定详细格式。所以非常少被採用。另外,从2000年開始,IEEE 754開始修订,被称为IEEE 754R(754r.ucbtest.org/)。目的是融合IEEE 754和IEEE 854标准。

IEEE 754-1985 和 IEEE 854-1987 均于 2008 年被 IEEE 754-2008 取代。

参考文献(图片均来源网络)

zhuanlan.zhihu.com/p/159127499

tc39.es/ecma262/#se...

developer.mozilla.org/zh-CN/docs/...

zh.wikipedia.org/wiki/%E6%95...

zh.wikipedia.org/wiki/IEEE_7...

相关推荐
喵叔哟14 分钟前
重构代码之取消临时字段
java·前端·重构
还是大剑师兰特1 小时前
D3的竞品有哪些,D3的优势,D3和echarts的对比
前端·javascript·echarts
王解1 小时前
【深度解析】CSS工程化全攻略(1)
前端·css
一只小白菜~1 小时前
web浏览器环境下使用window.open()打开PDF文件不是预览,而是下载文件?
前端·javascript·pdf·windowopen预览pdf
方才coding1 小时前
1小时构建Vue3知识体系之vue的生命周期函数
前端·javascript·vue.js
阿征学IT1 小时前
vue过滤器初步使用
前端·javascript·vue.js
王哲晓1 小时前
第四十五章 Vue之Vuex模块化创建(module)
前端·javascript·vue.js
丶21361 小时前
【WEB】深入理解 CORS(跨域资源共享):原理、配置与常见问题
前端·架构·web
发现你走远了1 小时前
『VUE』25. 组件事件与v-model(详细图文注释)
前端·javascript·vue.js
Mr.咕咕1 小时前
Django 搭建数据管理web——商品管理
前端·python·django