当你学习一个知识点没有方向时,可以尝试以解决问题的角度来理解它。 例如这个知识点我们可以从以下问题开始:
问题一我们需要知道 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 用 二进制表示也是 “1”,如果我们填充 64 位二进制,表示应该是怎样的呢?
那么实际我们看到的二进制存储应该是这样的: 0 01111111111 0000...0000 用在线转换工具转换后如下图,可以验证结果的正确性: http://www./result_double.html 我们再用 0.1 0.2 验证下: 0.1.toString(2) // 0.00011001100...11010 0.1 的二进制 64 位转成科学记数法表示:
我们可以发现,其实 0.1 的尾数是 1100 不断循环的,但是我们看到的最后 4 位是 1010 这是由于尾数只能保存 52 位,多余的部分会被舍弃,舍弃规则是 IEEE 754 规范所定义的:
另外,规范还约定,由于二进制的科学记数法永远是 1.几 开头,所以将 1 省略,这样尾数就有 53 位来表示。所以 0.1 的二进制尾数部分:11001100...11001 这样 53 位,最后一位 1 向偶数舍入即进 1,得到 1010,这样得到的数其实是比真实的 0.1 要大的。 同理,我们查看 0.2 的表示: 我们再来看看问题三,为什么最大安全整数是 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 需要注意的是
|
|