不折腾的前端,和咸鱼有什么区别php
| 目录 |
| --- |
| 一 目录 |
| 二 前言 |
| 三 问题复现 |
| 3.1 根源:IEEE 754 标准 |
| 3.2 复现:计算过程 |
| 3.3 扩展:数字安全 |
| 四 解决问题 |
| 4.1 toFixed() |
| 4.2 手写简易加减乘除 |
| 五 现成框架 |
| 六 参考文献 |html
返回目录
在平常工做计算中,咱们如履薄冰,可是 JavaScript 总能给咱们这样那样的 surprise~前端
若是小伙伴给出心里的结果:java
那么小伙伴会被事实狠狠地扇脸:git
console.log(0.1 + 0.2); // 0.30000000000000004 console.log(1 - 0.9); // 0.09999999999999998
为何会出现这种状况呢?我们一探究竟!github
返回目录
下面,咱们会经过探讨 IEEE 754 标准,以及 JavaScript 加减的计算过程,来复现问题。编程
返回目录
JavaScript 里面的数字采用 IEEE 754 标准的 64 位双精度浮点数。该规范定义了浮点数的格式,对于 64 位的浮点数在内存中表示,最高的 1 位是符号为,接着的 11 位是指数,剩下的 52 位为有效数字,具体:后端
s
表示,0 表示为正数,1 表示为负数;e
表示;f
表示。符号位决定一个数的正负,指数部分决定数值的大小,小数部分决定数值的精度。安全
IEEE 754 规定,有效数字第一位默认老是 1,不保存在 64 位浮点数之中。框架
也就是说,有效数字老是 1.XX......XX
的形式,其中 XX......XX
的部分保存在 64 位浮点数之中,最长可能为 52 位。
所以,JavaScript 提供的有效数字最长为 53 个二进制位(64 位浮点的后 52 位 + 有效数字第一位的 1)。
返回目录
经过 JavaScript 计算 0.1 + 0.2 时,会发生什么?
一、 将 0.1 和 0.2 换成二进制表示:
0.1 -> 0.0001100110011001...(无限) 0.2 -> 0.0011001100110011...(无限)
浮点数用二进制表达式是无穷的
二、 由于 IEEE 754 标准的 64 位双精度浮点数的小数部分最多支持 53 位二进制位,因此二者相加以后获得二进制为:
0.0100110011001100110011001100110011001100110011001100
由于浮点数小数位的限制,这个二进制数字被截断了,用这个二进制数转换成十进制,就成了 0.30000000000000004,从而在进行算数计算时产生偏差。
返回目录
在看完上面小数的计算不精确后,jsliang 以为有必要再聊聊整数,由于整数一样存在一些问题:
console.log(19571992547450991); // 19571992547450990 console.log(19571992547450991 === 19571992547450994); // true
是否是很惊奇!
由于 JavaScript 中 Number
类型统一按浮点数处理,整数也不能逃避这个问题:
// 最大值 const MaxNumber = Math.pow(2, 53) - 1; console.log(MaxNumber); // 9007199254740991 console.log(Number.MAX_SAFE_INTEGER); // 9007199254740991 // 最小值 const MinNumber = -(Math.pow(2, 53) - 1); console.log(MinNumber); // -9007199254740991 console.log(Number.MIN_SAFE_INTEGER); // -9007199254740991
即整数的安全范围是: [-9007199254740991, 9007199254740991]
。
超过这个范围的,就存在被舍去的精度问题。
固然,这个问题并不只仅存在于 JavaScript 中,几乎全部采用了 IEEE-745 标准的编程语言,都会有这个问题,只不过在不少其余语言中已经封装好了方法来避免精度的问题。
而由于 JavaScript 是一门弱类型的语言,从设计思想上就没有对浮点数有个严格的数据类型,因此精度偏差的问题就显得格外突出。
到此为止,咱们能够看到 JavaScript 在处理数字类型的操做时,可能会产生一些问题。
事实上,工做中还真会有问题!
某天我处理了一个工做表格的计算,而后次日被告知线上有问题,以后被产品小姐姐问话:
默哀三秒,产生上面的找到探索,最终找到下面的解决方案。
返回目录
下面尝试经过各类方式来解决浮点数计算的问题。
返回目录
toFixed()
方法使用定点表示法来格式化一个数值。
语法:numObj.toFixed(digits)
参数:digits
。小数点后数字的个数;介于 0 到 20(包括)之间,实现环境可能支持更大范围。若是忽略该参数,则默认为 0。
const num = 12345.6789; num.toFixed(); // '12346':进行四舍五入,不包括小数部分。 num.toFixed(1); // '12345.7':进行四舍五入,保留小数点后 1 个数字。 num.toFixed(6); // '12345.678900':保留小数点后 6 个数字,长度不足时用 0 填充。 (1.23e+20).toFixed(2); // 123000000000000000000.00 科学计数法变成正常数字类型
toFixed()
得出的结果是String
类型,记得转换Number
类型。
toFixed()
方法使用定点表示法来格式化一个数,会对结果进行四舍五入。
经过 toFixed()
咱们能够解决一些问题:
原加减乘数:
console.log(1.0 - 0.9); // 0.09999999999999998 console.log(0.3 / 0.1); // 2.9999999999999996 console.log(9.7 * 100); // 969.9999999999999 console.log(2.22 + 0.1); // 2.3200000000000003
使用
toFixed()
:
// 公式:parseFloat((数学表达式).toFixed(digits)); // toFixed() 精度参数须在 0 与20 之间 parseFloat((1.0 - 0.9).toFixed(10)); // 0.1 parseFloat((0.3 / 0.1).toFixed(10)); // 3 parseFloat((9.7 * 100).toFixed(10)); // 970 parseFloat((2.22 + 0.1).toFixed(10)); // 2.32
那么,讲到这里,问题来了:
parseFloat(1.005.toFixed(2))
会获得什么呢,你的反应是否是 1.01
?
然而并非,结果是:1
。
这么说的话,enm...摔!o(╥﹏╥)o
toFixed()
被证实了也不是最保险的解决方式。
返回目录
既然 JavaScript 自带的方法不能自救,那么咱们只能换个思路:
/** * @name 检测数据是否超限 * @param {Number} number */ const checkSafeNumber = (number) => { if (number > Number.MAX_SAFE_INTEGER || number < Number.MIN_SAFE_INTEGER) { console.log(`数字 ${number} 超限,请注意风险!`); } }; /** * @name 修正数据 * @param {Number} number 须要修正的数字 * @param {Number} precision 端正的位数 */ const revise = (number, precision = 12) => { return +parseFloat(number.toPrecision(precision)); } /** * @name 获取小数点后面的长度 * @param {Number} 须要转换的数字 */ const digitLength = (number) => { return (number.toString().split('.')[1] || '').length; }; /** * @name 将数字的小数点去掉 * @param {Number} 须要转换的数字 */ const floatToInt = (number) => { return Number(number.toString().replace('.', '')); }; /** * @name 精度计算乘法 * @param {Number} arg1 乘数 1 * @param {Number} arg2 乘数 2 */ const multiplication = (arg1, arg2) => { const baseNum = digitLength(arg1) + digitLength(arg2); const result = floatToInt(arg1) * floatToInt(arg2); checkSafeNumber(result); return result / Math.pow(10, baseNum); // 整数安全范围内的两个整数进行除法是没问题的 // 若是有,证实给我看 }; console.log('------\n乘法:'); console.log(9.7 * 100); // 969.9999999999999 console.log(multiplication(9.7, 100)); // 970 console.log(0.01 * 0.07); // 0.0007000000000000001 console.log(multiplication(0.01, 0.07)); // 0.0007 console.log(1207.41 * 100); // 120741.00000000001 console.log(multiplication(1207.41, 100)); // 0.0007 /** * @name 精度计算加法 * @description JavaScript 的加法结果存在偏差,两个浮点数 0.1 + 0.2 !== 0.3,使用这方法能去除偏差。 * @param {Number} arg1 加数 1 * @param {Number} arg2 加数 2 * @return arg1 + arg2 */ const add = (arg1, arg2) => { const baseNum = Math.pow(10, Math.max(digitLength(arg1), digitLength(arg2))); return (multiplication(arg1, baseNum) + multiplication(arg2, baseNum)) / baseNum; } console.log('------\n加法:'); console.log(1.001 + 0.003); // 1.0039999999999998 console.log(add(1.001, 0.003)); // 1.004 console.log(3.001 + 0.07); // 3.0709999999999997 console.log(add(3.001, 0.07)); // 3.071 /** * @name 精度计算减法 * @param {Number} arg1 减数 1 * @param {Number} arg2 减数 2 */ const subtraction = (arg1, arg2) => { const baseNum = Math.pow(10, Math.max(digitLength(arg1), digitLength(arg2))); return (multiplication(arg1, baseNum) - multiplication(arg2, baseNum)) / baseNum; }; console.log('------\n减法:'); console.log(0.3 - 0.1); // 0.19999999999999998 console.log(subtraction(0.3, 0.1)); // 0.2 /** * @name 精度计算除法 * @param {Number} arg1 除数 1 * @param {Number} arg2 除数 2 */ const division = (arg1, arg2) => { const baseNum = Math.pow(10, Math.max(digitLength(arg1), digitLength(arg2))); return multiplication(arg1, baseNum) / multiplication(arg2, baseNum); }; console.log('------\n除法:'); console.log(0.3 / 0.1); // 2.9999999999999996 console.log(division(0.3, 0.1)); // 3 console.log(1.21 / 1.1); // 1.0999999999999999 console.log(division(1.21, 1.1)); // 1.1 console.log(1.02 / 1.1); // 0.9272727272727272 console.log(division(1.02, 1.1)); // 数字 9272727272727272 超限,请注意风险!0.9272727272727272 console.log(1207.41 / 100); // 12.074100000000001 console.log(division(1207.41, 100)); // 12.0741 /** * @name 按指定位数四舍五入 * @param {Number} number 须要取舍的数字 * @param {Number} ratio 精确到多少位小数 */ const round = (number, ratio) => { const baseNum = Math.pow(10, ratio); return division(Math.round(multiplication(number, baseNum)), baseNum); // Math.round() 进行小数点后一位四舍五入是否有问题,若是有,请证实出来 // https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Math/round } console.log('------\n四舍五入:'); console.log(0.105.toFixed(2)); // '0.10' console.log(round(0.105, 2)); // 0.11 console.log(1.335.toFixed(2)); // '1.33' console.log(round(1.335, 2)); // 1.34 console.log(-round(2.5, 0)); // -3 console.log(-round(20.51, 0)); // -21
在这份代码中,咱们先经过石锤乘法的计算,经过将数字转成整数进行计算,从而产生了 安全 的数据。
JavaScript 整数运算会不会出问题呢?
乘法计算好后,假设乘法已经没问题,而后经过乘法推出 加法、减法 以及 除法 这三则运算。
最后,经过乘法和除法作出四舍五入的规则。
JavaScript
Math.round()
产生的数字会不会有问题呢、
这样,咱们就搞定了两个数的加减乘除和四舍五入(保留指定的长度),那么,里面会不会有问题呢?
若是有,请例举出来。
若是没有,那么你能不能依据上面两个数的加减乘除,实现三个数甚至多个数的加减乘除?
返回目录
这么重要的计算,若是本身写的话你总会感受惶惶不安,感受充满着危机。
因此不少时候,咱们可使用大佬们写好的 JavaScript 计算库,由于这些问题大佬已经帮咱们进行了大量的测试了,大大减小了咱们手写存在的问题,因此咱们能够调用别人写好的类库。
下面推荐几款不错的类库:
Math.js 是一个用于 JavaScript 和 Node.js 的扩展数学库。
它具备支持符号计算的灵活表达式解析器,大量内置函数和常量,并提供了集成的解决方案来处理不一样的数据类型,例如数字,大数,复数,分数,单位和矩阵。
强大且易于使用。
JavaScript 的任意精度的十进制类型。
一个小型,快速,易于使用的库,用于任意精度的十进制算术运算。
一个用于任意精度算术的 JavaScript 库。
最后的最后,值得一提的是:若是对数字的计算很是严格,或许你能够将参数丢给后端,让后端进行计算,再返回给你结果。
例如涉及到比特币、商城商品价格等的计算~
返回目录
致敬在 JavaScript 计算这块领域作了贡献的大佬,本篇文章大致采用了如下文章的内容,对其进行了我的尝试和融汇,感谢大佬们的文献:
若是你想了解更多,欢迎关注 jsliang 的文档库:document-library
<img alt="知识共享许可协议" style="border-width:0" src="https://user-gold-cdn.xitu.io/2019/11/21/16e8ba77d56b35d5?w=88&h=31&f=png&s=1888" />
<span xmlns:dct="http://purl.org/dc/terms/" property="dct:title">jsliang 的文档库</span> 由 梁峻荣 采用 知识共享 署名-非商业性使用-相同方式共享 4.0 国际 许可协议进行许可。
基于 https://github.com/LiangJunro...上的做品创做。
本许可协议受权以外的使用权限能够从 https://creativecommons.org/l... 处得到。