// 1. 两数相加 // 0.1 + 0.2 = 0.30000000000000004 // 0.7 + 0.1 = 0.7999999999999999 // 0.2 + 0.4 = 0.6000000000000001 // 2.22 + 0.1 = 2.3200000000000003 // 2. 两数相减 // 1.5 - 1.2 = 0.30000000000000004 // 0.3 - 0.2 = 0.09999999999999998 // 3. 两数相乘 // 19.9 * 100 = 1989.9999999999998 // 19.9 * 10 * 10 = 1990 // 1306377.64 * 100 = 130637763.99999999 // 1306377.64 * 10 * 10 = 130637763.99999999 // 0.7 * 180 = 125.99999999999999 // 4. 不同的数却相等 // 1000000000000000128 === 1000000000000000129
计算机的底层实现就没法彻底精确表示一个无限循环的数,并且可以存储的位数也是有限制的,因此在计算过程当中只能舍去多余的部分,获得一个尽量接近真实值的数字表示,因而形成了这种计算偏差。html
好比在 JavaScript 中计算0.1 + 0.2
时,十进制的0.1和0.2都会被转换成二进制,但二进制并不能彻底精确表示转换结果,由于结果是无限循环的。前端
// 百度进制转换工具 0.1 -> 0.0001100110011001... 0.2 -> 0.0011001100110011...
In JavaScript, Number is a numeric data type in the double-precision 64-bit floating point format (IEEE 754). In other programming languages different numeric types can exist, for examples: Integers, Floats, Doubles, or Bignums.git
根据 MDN这段关于Number的描述 能够得知,JavaScript 里的数字是采用 IEEE 754 标准的 64 位双精度浮点数。该规范定义了浮点数的格式,最大最小范围,以及超过范围的舍入方式等规范。因此只要不超过这个范围,就不会存在舍去,也就不会存在精度问题了。好比:github
// Number.MAX_SAFE_INTEGER 是 JavaScript 里能表示的最大的数了,超出了这个范围就不能保证计算的准确性了 var num = Number.MAX_SAFE_INTEGER; num + 1 === num +2 // = true
实际工做中咱们也用不到这么大的数或者是很小的数,也应该尽可能把这种对精度要求高的计算交给后端去计算,由于后端有成熟的库来解决这个计算问题。前端虽然也有相似的库,可是前端引入一个这样的库代价太大了。web
排除直接使用的数太大或过小超出范围,出现这种问题的状况基本是浮点数的小数部分在转成二进制时丢失了精度,因此咱们能够将小数部分也转换成整数后再计算。网上不少帖子贴出的解决方案就是这种:后端
var num1 = 0.1 var num2 = 0.2 (num1 * 10 + num2 * 10) / 10 // = 0.3
可是这样转换整数的方式也是一种浮点数计算,在转换的过程当中就可能存在精度问题,好比:数组
1306377.64 * 10 // = 13063776.399999999 1306377.64 * 100 // = 130637763.99999999
var num1 = 2.22; var num2 = 0.1; (num1 * 10 + num2 * 10) / 10 // = 2.3200000000000003
因此不要直接经过计算将小数转换成整数,咱们能够经过字符串操做,移动小数点的位置来转换成整数,最后再一样经过字符串操做转换回小数:wordpress
/** * 经过字符串操做将一个数放大或缩小指定倍数 * @num 被转换的数 * @m 放大或缩小的倍数,为正表示小数点向右移动,表示放大;为负反之 */ function numScale(num, m) { // 拆分整数、小数部分 var parts = num.toString().split('.'); // 原始值的整数位数 const integerLen = parts[0].length; // 原始值的小数位数 const decimalLen = parts[1] ? parts[1].length : 0; // 放大,当放大的倍数比原来的小数位大时,须要在数字后面补零 if (m > 0) { // 补多少个零:m - 原始值的小数位数 let zeros = m - decimalLen; while (zeros > 0) { zeros -= 1; parts.push(0); } // 缩小,当缩小的倍数比原来的整数位大时,须要在数字前面补零 } else { // 补多少个零:m - 原始值的整数位数 let zeros = Math.abs(m) - integerLen; while (zeros > 0) { zeros -= 1; parts.unshift(0); } } // 小数点位置,也是整数的位数: // 放大:原始值的整数位数 + 放大的倍数 // 缩小:原始值的整数位数 - 缩小的倍数 var index = integerLen + m; // 将每一位都拆到数组里,方便插入小数点 parts = parts.join('').split(''); // 当为缩小时,由于可能会补零,因此使用原始值的整数位数 // 计算出的小数点位置可能为负,这个负数应该正好是补零的 // 个数,因此小数点位置应该为 0 parts.splice(index > 0 ? index : 0, 0, '.'); return parseFloat(parts.join('')); }
/** * 获取小数位数 */ function getExponent(num) { return Math.floor(num) === num ? 0 : num.toString().split('.')[1].length; } /** * 两数相加 */ function accAdd(num1, num2) { const multiple = Math.max(getExponent(num1), getExponent(num2)); return numScale(numScale(num1, multiple) + numScale(num2, multiple), multiple * -1); }
测试用例:工具
describe('accAdd', function() { it('(0.1, 0.2) = 0.3', function() { assert.strictEqual(0.3, _.accAdd(0.1, 0.2)) }) it('(2.22, 0.1) = 2.32', function() { assert.strictEqual(2.32, _.accAdd(2.22, 0.1)) }) it('(11, 11) = 22', function() { assert.strictEqual(22, _.accAdd(11, 11)) }) })