javascript的数值都是双精度的

2019/04/10

二进制的转化

计算机表达值都是通过0和1,即二进制,稍微了解过二进制的人都很清楚二进制和十进制转化其实是非常简单的,但是这个转换规则也是非常重要的,这是计算机的最基本的特性。

计算机通常会有很多规范来规定0和1如何表达数值,比如整数的规定中就需要第一位表示正负,其余位数表示值。

所以64位的电脑可以处理的最大整数就是 1*2^63-1,63次方是因为第一位表示正负了,需要减1是因为

111 === 1*2^2 + 1*2^1 + 1*2^0 === 7 === 2^3-1

二进制转十进制

2进制转化10进制非常简单,比如11.01就是

1*2^1 + 1*2^0 + 0*2^-1 + 1*2^-2 = 3.25

小数点会通过2^-1,这样的方式计算。

整数的十进制转二进制

想象10用二进制表示是

1*2^3 + 0*2^2 + 1*2^1 + 0*2^0 即 1010

有一个小技巧

就是

10 * 2 = 5 ... 0

5 * 2 = 2 ... 1

2 * 2 = 1 ... 0

1 * 2 = 0 ... 1

那么后边的余数0101倒过来就是1010,这个就是转十进制转二进制的一个技巧。

小数的十进制转二进制

小数的转化也有一个技巧

0.1 * 2 = 0.2

0.2 * 2 = 0.4

0.4 * 2 = 0.8

0.8 * 2 = 1.6

0.6 * 2 = 1.2

0.2 * 2 = 0.4

0.4 * 2 = 0.8

...

...

这就进入一个循环了,所以二进制的0.1是多少呢,000110011001100110011001100...

就是说二进制表示0.1是一个无限且循环的0和1。

用转化工具试一下如下


同样0.2,0.3,0.4都是包含循环的01,而0.5用二进制表达不会出现循环小数,所以是非常精确的。

双精度和单精度

对于正常人来说,小数和整数的区别可能只是在一个点上,但是计算机并没有小数点,所以只能制定规范,去规定如何表达一个小数。这个规范叫做IEEE754。

一般的情况下计算机都是用浮点数表示小数,浮点数的小数点位置一直会被移到第一位后边。比如十进制的123.4会把小数点移到第一位后边变成1.234 * 10^3,那么移动小数点,自然就需要用指数来表示位数,小数和整数一样都需要用一位来表示正负。所以浮点数由三部分组成正负,有效值和指数。

而浮点数根据精确度不同包括单精度或者双精度。为什么需要两种,还是空间的原因,比如说表示一个数1,那么只需要和电脑要一个“格子”就好了。所以整数也分为整型和长整型,小数也分为单精度和双精度,双精度的精确度更高,但需要的“格子”也更多。

偷了两张知乎的图过来,可以很好的知道二者的区别。

单精度

双精度

双精度用64bits表达一个小数,就是说定义一个float就是和计算机要64bits的空间,会花1位来表示正负,11位表示指数,52位表示有效的数值。

比如说0.15用双精度表示就是1.5*10^-2,看上边的图说就是一个格子是1表示正,然后用11个格子表示-2,然后用52个格子表示1.5。

这里存在三个问题需要清楚

  1. 52个“格子”表示有效值,但其实是53个“格子”,为什么呢,因为科学计数法表示二进制总会有一个1在最前面。所以第一个1不需要在这个52个“格子”里放着浪费空间,因为双精度浮点数的最大有效值是2^53 - 1
  2. 11个格子表示指数,这个指数仍然有正负的关系,所以指数的最大值是2^10 - 1即1023。为什么要这么大的指数,这也是浮点数的优点,可以表示超级大的数,但是有效值比同样空间的整数要小的多,64个“格子”可以表示最大的有效整数是2^63 - 1。
  3. 这个顺序是反序,计算机先存有效值,然后存指数,然后存符号

顺便提一下,64位的电脑其实是说可以最大同时处理64个“格子”运算,所以电脑可以处理的最大值也是2^63 - 1

上边也说了0.1, 0.2之类的数字在二进制里是一个循环小数,这意味着52位是存不下的,存不下怎么办呢,十进制有四舍五入,二进制其实是0舍1入。

就是说

1.001011保留三位小数就是1.001

1.001011保留四位小数会由于第五位是1所以进1,即1.0011

那么0.1在双精度如何存的呢

一大波01高能预警

首先转化为二进制是

0.00011001100110011001100110011001100110011001100110011001100110011001100

科学计数法会是1.1001100110011001100110011001100110011001100110011001100110011 * 10^-4

所以存起来的第一位会被舍去,然后第53位四舍五入之后取52位可以得到有效位的数字就是下边这个。

1001100110011001100110011001100110011001100110011011

然后-4在指数位就是00000000100

最后一位是1表示正

全部表示下来就是这个样子

1001100110011001100110011001100110011001100110011011000000001001

emmm,这个数字没办法计算,所以还是表示成

0.0001001100110011001100110011001100110011001100110011011这样方便计算点。

而0.2的话是0.001001100110011001100110011001100110011001100110011011。

感受一下这个相加的过程

const odot2 = `0.001100110011001100110011001100110011001100110011001101`
const odot1 = `0.0001100110011001100110011001100110011001100110011001101`
const plus3 = `0.010011001100110011001100110011001100110011001100110100`

这个相加的值就是0.30000000000000004,这也是为什么加起来会出错,因为0.1和0.2再第53位都进1了。然后导致两个数都略大于本身。

javascript中的数值存在的问题

截止目前为止都并没有提及javascript的数值这个概念,其实是因为上边的问题是再很多语言中都存在的,并不是js的特性。现在来专门说说js。

其实很多语言可以定义整形,长整形来保存不同的整数,因为小的数字占用的空间更小。

但javascript并所有的数值都只能使用float,即双精度,尽管ES6提供了Number.isInteger来判断一个数值是不是整数,但这个并也不是一个严格意义上的整型的整数,因为事实上这个整数的保存方式是float。

javascript的最大安全整数是即2^53 - 1,这个也是因为有效值53位的原因。

Number.MAX_SAFE_INTEGER  // 9007199254740991

当数值大于这个值的时候就会出现各种各样的问题。比如下边的这些问题。分别是

1. 大于最大值会存在四舍五入(并不是四舍五入,而是二进制的0舍1入)

2. 大于最大的小数后边的小数位被忽略,所以无法判断是整数还是小数

3. 四舍五入的原因导致比大小时出错

console.log(123456789012345678) // 123456789012345680
console.log(Number.isInteger(123456789012345678.123)) // true
console.log(123456789012345678 === 123456789012345680) // true

精度也会导致超级经典的0.1 + 0.2的问题

console.log(0.1 + 0.2 !== 0.3) // true

但有效位是16依然可以表示下边这种非常小的数值,是因为这个数值的有效位只有525可以很容易表示。

console.log(0.0000000000000000000000000000000000000000000000000000000525)
// 5.25e-56

因为指数最大是1023,所以js的最大值是

Math.pow(2, 1023) - 1

如果把这个值输出出来会是Infinity,这是因为前边计算已经是无穷了,无穷 - 1仍然是无穷。

如何表示最大值呢,下边那种方式可以一直无限逼近最大值,但是事实上是表示不出来的。因为计算会出现误差。

console.log(Number.MAX_VALUE) // 1.7976931348623157e+308
console.log(
Math.pow(2, 1023) +
Math.pow(2, 1022) +
Math.pow(2, 1021) +
Math.pow(2, 1020) +
Math.pow(2, 1019) +
Math.pow(2, 1018) +
Math.pow(2, 1017) +
Math.pow(2, 1016) +
Math.pow(2, 1015)
) // 1.794182015458288e+308

负无穷和正无穷在js中可以这么表示

console.log(Number.NEGATIVE_INFINITY)
console.log(Number.POSITIVE_INFINITY)

js中有一个专门表示精度的常量

console.log(Math.abs(0.1 + 0.2 - 0.3) < Number.EPSILON) // true
console.log(Math.abs(0.4 + 0.2 - 0.6) < Number.EPSILON) // true

这个常量的意义就是判断误差是否是因为双精度的精确导致的,如果比精度还小,那么这个误差就是可以忽略的。

嗨,请先登录

加载中...
(๑>ω<๑) 又是元气满满的一天哟