0.30000000000000004问题是计算机科学领域的经典BUG, 由比尔盖茨那一代人标准化的浮点数表示法造福了一代人也祸害了一代人, 由此引出了不少的坑, 比如大多数编程语言中0.1+0.2==0.30000000000000004.遇到这个问题不要担心, 你的编译环境没有坏, 只是计算机在做进制转换的时候需要绕一些丸子, 本文来具体分析一下这个bug背后的秘密, 也可以访问它的官解: http://0.30000000000000004.com/
众所周知JS仅有Number这个数值类型,而Number采用的时IEEE 754 64位双精度浮点数编码。而浮点数表示方式具有以下特点:
1. 浮点数可表示的值范围比同等位数的整数表示方式的值范围要大得多;
2. 浮点数无法精确表示其值范围内的所有数值,而有符号和无符号整数则是精确表示其值范围内的每个数值;
3. 浮点数只能精确表示m*2e的数值;
4. 当biased-exponent为2e-1-1时,浮点数能精确表示该范围内的各整数值;
5. 当biased-exponent不为2e-1-1时,浮点数不能精确表示该范围内的各整数值。
由于部分数值无法精确表示(存储),于是在运算统计后偏差会愈见明显。
不仅是JavaScript会产生这种问题,只要是采用IEEE 754 Floating-point的浮点数编码方式来表示浮点数时,则会产生这类问题。绝对完美的数值编码方案是不存在的,为了处理方便,这个标准引入了大量的折衷和妥协,建立在这种表达方式上的算法(例如除法运算)也一样。由于数值表达方式存在“缺陷”,运算结果不可避免地堆聚起越来越多的误差, 补丁越来越多, 浮点算法就成了和路由协议一样的筛子。下面我们来分析整个运算过程。
1. 0.1 的二进制表示为 1.1001100110011001100110011001100110011001100110011001 1(0011)+ * 2^-4;
2. 当64bit的存储空间无法存储完整的无限循环小数,而IEEE 754 Floating-point采用round to nearest, tie to even的舍入模式,因此0.1实际存储时的位模式是0-01111111011-1001100110011001100110011001100110011001100110011010;
3. 0.2 的二进制表示为 1.1001100110011001100110011001100110011001100110011001 1(0011)+ * 2^-3;
4. 当64bit的存储空间无法存储完整的无限循环小数,而IEEE 754 Floating-point采用round to nearest, tie to even的舍入模式,因此0.2实际存储时的位模式是0-01111111100-1001100110011001100110011001100110011001100110011010;
5. 实际存储的位模式作为操作数进行浮点数加法,得到 0-01111111101-0011001100110011001100110011001100110011001100110100。转换为十进制即为0.30000000000000004。
扩展一下, 就有了多种类似的产生式:
Why 0.7 * 180===125.99999999998?
1. 0.7实际存储时的位模式是0-01111111110-0110011001100110011001100110011001100110011001100110;
2. 180实际存储时的位模式是0-10000000110-0110100000000000000000000000000000000000000000000000;
3. 实际存储的位模式作为操作数进行浮点数乘法,得到0-10000000101-1111011111111111111111111111111111111111101010000001。转换为十进制即为125.99999999998。
Why 1000000000000000128
=== 1000000000000000129
?
1. 1000000000000000128
实际存储时的位模式是0-10000111010-1011110000010110110101100111010011101100100000000001;
2. 1000000000000000129
实际存储时的位模式是0-10000111010-1011110000010110110101100111010011101100100000000001;
3. 因此1000000000000000128和
1000000000000000129的实际存储的位模式是一样的。
日常开发规范
■ 大多数Web页面不需要小数
避免使用小数,尽量设法使用整数。确保数组的索引都是整数。按分(而不是元)计算金额。百分比放大100倍计算以避免出现小数。尽可能不用除法(/)和模(%)运算,因为大多数情况下它们直接导致出现浮点数。如果必须使用除法,立即用Math.round方法回归整数运算。
■ 如果必须使用浮点数,则尽可能引入冗余小数位——即在程序要求的运算精度之外,再增加小数位
如果程序需要5位数字的小数精度,则在运算中至少保留6位的小数,8位更好。冗余位越多,累计误差的影响越小。
■ 避免在同一个表达式中使用相差太大或太小的数值
对两个非常接近的数值执行减法或比较操作很容易出错。将很小的数值和很大数值相加无异于浪费时间,小的数值很可能被当作0。不过,很小的数值乘以很大的数值一般不会出现问题,例如2 * 12345678会得到正确的结果24691356。但是,0.1 - 0.09的结果是0.010000000000000009。
■ 用isFinite()和isNaN()检查运算结果
通过表单提交任何数值运算结果之前,一定要先检查数据的合法性。
■ 慎用数值运算
程序涉及的数值运算越少,引入误差的可能就越小。视浮点数为贵客,不可任意驱使。