这个问题应该是在浮点数运算里比较常见的问题了,
这次就送三个方面来分析一下这个问题:
- 解决方案
- 为什么会有这个问题
- 一个表示浮点数的工具
常用解决方案
- 将浮点数扩大$10^n$变成整数,进行整数运算
- if判断操作时使用
Number.EPSILON
可表示(representable)数之间的最小间隔1
20.1 + 0.2 == 0.3 //false
0.1 + 0.2 - 0.3 < Number.EPSILON
为什么会有这个问题
ECMAScript7种常见类型中,Number应该是最令人关注的数据类型了,这种类型使用IEEE754格式来表示整数和浮点数。所以浮点数计算会产生舍入误差的问题,是IEEE754数值的浮点问题,不仅仅存在于javaScript中,也存在于任何一个使用IEEE754标准的语言中。
由于计算机存储和计算都是基于二进制,但是我们平时日常使用和编程使用的大都是十进制表示。所以这就存在一个二进制转十进制的问题。0.1+0.2≠0.3的问题就是来源于此。
具体来说,在二进制表示十进制整数时并没有什么问题,但是在表示浮点数时,便会出现精度问题。
javaScript采用IEEE754标准的双精度64位二进制的形式(单精度采用1+8+23位的二进制表示)。
IEEE754
基于 IEEE 754 标准的双精度 64 位二进制格式的值(-($2^{63}$ -1) 到 $2^{63}$ -1)。它并没有为整数给出一种特定的类型。除了能够表示浮点数外,还有一些带符号的值:+Infinity,-Infinity 和 NaN (非数值,Not-a-Number)。
S | Exp | Fraction | |
---|---|---|---|
长度 | 1 | 11 | 52位长 |
位数 | 63 | 62至52 | 偏正值(实际的指数大小+1023) 51至0位编号(从右边开始为0) |
$1.fraction^{exponent}$
第0位记录符号位
- 1 负数
- 0 正数
第1-11位记录指数偏移值(exponent)
- 为了兼容正负数,采用移码中的阶码的方式表示(详见2.5.4)
第12-63位存储有效位(fraction)
转换步骤
- 将十进制转为二进制表示(误差来自于这个步骤)
- 将二进制转换为指数科学计数法
- 将科学计数法采用IEEE754的方式存储于计算机
举例来说,为了方便说明,我们这里选择 77.1875其小数部分是可以用有限位表示的
将十进制转换为二进制
77 => 1001101
0.1875 => 0.0011
77.1875 => 1001101.0011将二进制转化为指数科学计数法
1001101.0011 => 1.0011010011*$2^6$将科学计数法采用IEEE754的方式存储于计算机
- 符号位: + => 0
- 指数偏移量 => 6+2047 => 110
- 有效位:0011010011
- 变为64位表示
0 00000000110 0000000000 0000000000 0000000000 0000000000 0000110100 11
在日常开发中,将小数部分转化为二进制,常常无法在有限位内转化,所以52位有效位的情况下,从小数点后第一个非0数字开始,之后最多可以保留52位(加上最左边被舍去的1,应该是53位)。
js里的最大整数和浮点数
- Number.MAX_SAFE_INTEGER
- JavaScript 中最大的安全整数 ($2^{53}$ - 1)。
- 52位二进制表示的最大数就是$2^{53}$ - 1
- Number.MAX_VALUE
- 能表示的最大正数。最小的负数是 -MAX_VALUE。
- Number.MIN_SAFE_INTEGER
- JavaScript 中最小的安全整数 (-($2^{53}$ - 1)).
- Number.MIN_VALUE
- 能表示的最小正数即最接近 0 的正数 (实际上不会变成 0)。最大的负数是 -MIN_VALUE。
- 二进制表示为 0.0000000000000……00001
- 根据IEEE754标准,小数点后最多可以出现$2^{11}$
要检查值是否大于或小于 +/-Infinity,你可以使用常量 Number.MAX_VALUE 和 Number.MIN_VALUE。另外在 ECMAScript 6 中,你也可以通过 Number.isSafeInteger() 方法还有 Number.MAX_SAFE_INTEGER 和 Number.MIN_SAFE_INTEGER($2^{53}$-1) 来检查值是否在双精度浮点数的取值范围内。 超出这个范围,JavaScript 中的数字不再安全了,也就是只有 second mathematical interger 可以在 JavaScript 数字类型中正确表现。
js里常见的数据表示法
- 二进制
- 以0b开头
- 0b101 (十进制的5)
- 八进制
- 以0o开头
- 0o101 (十进制的65)
- 十进制
- 日常编码最常用的进制
- 十六进制
- 以0x开头
- 0x101 (十进制257)
补码的方式
负数补码表示的范围比原码稍宽,多一种数码组合。对于定点数,若为纯小数,表示范围为:
-1 ~ $1-2^n$。若为纯整数,表示范围为:$-2^n$ ~ $2^n-1$(原码:$-2^n+1$ ~ $2^n-1$)
原码转换补码
(以8位二进制为例)
- 正整数的补码是其二进制表示,与原码相同
- 9 =>
0000 1001
- 9 =>
- 负整数的补码,将其原码除符号位外的所有位取反(0变1,1变0,符号位为1不变)后加1
- 原码 -9 =>
1000 1001
- 取反
1000 1001
=>1111 0110
- 加一
1111 0110
=>1111 0111
- 原码 -9 =>
- 数0的补码表示是唯一的
- 原码会有
+0
、-0
的问题 0000 0000
- 原码会有
补码转为原码
- 如果补码的符号位为“0”,表示是一个正数,其原码就是补码。
- 如果补码的符号位为“1”,表示是一个负数,求这个补码的补码就是原码。
- 取反
1111 0111
=>1000 1000
- 加一
1000 1000
=>1000 1001
- 原码
1000 1001
=>-9
- 取反
- 0是唯一的
0000 0000
- 原码的-0 =>
1000 0000
表示的是什么?来看一下
- 取反
1000 0000
=>1111 1111
- 加一
1111 1111
=>1000 0000
- 原码
1000 0000
=>-128
(符号位也代表了数值)
- 原码的-0 =>
补码的意义
- 解决了符号的表示的问题;
- 可以将减法运算转化为补码的加法运算来实现,克服了原码加减法运算繁杂的弊端,可有效简化运算器的设计;
这里需要展开一下,以八位二进制来说,- 6 和 6 + 128 ($2^8$)的表示是一样的
0000 0110
- 利用上述的特点就可以将减法操作变为加法操作
- 9-3 => 9-3+128 => 9+125
- 所以将-3转化为125可以简化运算
- 这个转化关系就是补码 取反加一
- 6 和 6 + 128 ($2^8$)的表示是一样的
- 在计算机中,利用电子器件的特点实现补码和真值、原码之间的相互转换,非常容易;
- 补码表示统一了符号位和数值位,使得符号位可以和数值位一起直接参与运算,这也为后面设计乘法器除法器等运算器件提供了极大的方便。
总之,补码概念的引入和当时运算器设计的背景不无关系,从设计者角度,既要考虑表示的数的类型(小数、整数、实数和复数)、数值范围和精确度,又要考虑数据存储和处理所需要的硬件代价。因此,使用补码来表示机器数并得到广泛的应用,也就不难理解了
阶码
在IEEE274中,指数部分采用的移码方式是阶码,而不是传统的补码,阶码的规则更加简单,就是用 $2^{n-1}-1$ + 实际值就是最终的结果。
比如,一个8位的二进制($2^{n-1}-1$ = 127),表示的数是 39
实际转码就是 127+39 = 166;
表示的数是 -56;
实际转码就是 127-56 = 71;
一个表示浮点数的工具
如代码无法运行请点击这里