# JavaScript 中的数值
在 JavaScript 中的 Number 对象是经过封装能让你处理数字值的对象,其中数字的类型就是为双精度 IEEE 754 64 位浮点类型。
因此,JavaScript 能够准确表示的整数范围在 (-2^53, 2^53)
之间,超过这个范围,无法精确表示这个整数。
在解析序列化的 JSON 时,如果 JSON 解析器将它们强制转换为 Number 类型,那么超出此范围的整数值可能会被破坏。在工作中使用 String 类型代替,是一个可行的解决方案。
值得注意的是,最近推出的 BigInt 任意精度数字类型提供了一种方法来表示大于 $2^53 - 1$ 的整数,它可以表示任意大的整数。
# 位运算
除了一些常规的算术运算,在 JavaScript 中也支持一些按位操作:
操作名称 | 运算符 | 说明 |
---|---|---|
按位与 | & | 两个操作数对应的位都是 1 时返回 1 |
按位或 | | | 其中一个操作数对应的位是 1 时就返回 1 |
按位异或 | ^ | 仅当其中一个操作数对应的位是 1 时就返回 1 |
按位非 | ~ | 按位取反,1 成为 0,0 变为 1 |
按位左移运算符 | << | 将第一个操作数向左移动指定的位数。向左被移出的位被丢弃,右侧用 0 补充 |
按位右移运算符 | >> | 将第一个操作数向右移动指定的位数。向右被移出的位被丢弃,拷贝最左侧的位以填充左侧 |
按位无符号右移运算符 | >>> | 将第一个操作数向右移动指定的位数。向右被移出的位被丢弃,左侧用 0 填充 |
在无符号右移中结果总是非负的,即使右移 0 个比特,结果也是非负的。
另外,由于按位非只在 -1 时返回 0,所以可以结合 indexOf()
方法来做逻辑判断,而按位与经常可以用来做去零操作。
# 0.1 + 0.2 != 0.3
为什么 0.1 + 0.2 在 JavaScript 中不等于 0.3?
事实上,所有采用双精度 IEEE 754 64 位浮点数类型的语言都存在这个问题。因为这并不是语言本身的问题,而是因为浮点数计算标准的要求。
在生活中,我们习惯使用的有十进制,另外还有八进制、十二进制、十六进制、六十进制等等,而计算机使用的是二进制。
所以,计算机在存储和计算像 0.1 和 0.2 这样的十进制的数字前要进行二进制转换。按照 IEEE 754 标准,十进制的 0.1 和 0.2 转换成二进制数值都会是无限循环的值:
0.1 -> 0.0001100110011001...(循环)
0.2 -> 0.0011001100110011...(循环)
根据 IEEE 754 双精度标准,尾数最多能存储 53 位有效数字,那么就必须在特定的位置进行四舍五入处理,最后的结果是:
0.1 -> 1.1001100110011001100110011001100110011001100110011010 * 2 ** -4
-> 0000011110111001100110011001100110011001100110011001100110011010
0.2 -> 1.1001100110011001100110011001100110011001100110011010 * 2 ** -3
-> 0000011111001001100110011001100110011001100110011001100110011010
可见两位数字在转换后都存在一定的精度损失,接下来就是运算之前的对阶。
# 对阶
所谓对阶就是将两个进行运算的浮点数的阶码对齐的操作,目的是为使两个浮点数的尾数能够进行加减运算。
当进行 Mx·2Ex
与 My·2Ey
加减运算时,首先求出两浮点数阶码的差,即 ⊿E=Ex-Ey
,将小阶码加上 ⊿E
,使之与大阶码相等,同时将小阶码对应的浮点数的尾数右移相应位数,以保证该浮点数的值不变。
- 对阶的原则是小阶对大阶,之所以这样做是因为若大阶对小阶,则尾数的数值部分的高位需移出,而小阶对大阶移出的是尾数的数值部分的低位,这样损失的精度更小。
- 若
⊿E=0
,说明两浮点数的阶码已经相同,无需再做对阶操作了。 - 采用补码表示的尾数右移时,符号位保持不变。
- 由于尾数右移时是将最低位移出,会损失一定的精度,为减少误差,可先保留若干移出的位,供以后舍入处理用。
对 0.1 和 0.2 来说,0b00001111100 比 0b00001111011 大 1,所以需要将 0.1 对应的尾数向右移一位:
0.1 -> 0000011110111001100110011001100110011001100110011001100110011010
-> 0000011111001100110011001100110011001100110011001100110011001101
# 尾数求和
经过对阶之后,现在将其尾数与 0.2 的尾数进行直接相加。
需要注意的是经过右移之后 0.1 尾数中隐藏的已经出现了,而 0.2 尾数中隐藏的 1 还在,所以也要进行相加。
0.1100110011001100110011001100110011001100110011001101
+ 1.1001100110011001100110011001100110011001100110011010
______________________________________________________
10.0110011001100110011001100110011001100110011001100111
# 规格化
规格化又叫做规格化数,是一种表示浮点数的规格化的表示方法,还可以通过修改阶码并同时移动尾数的方法使其满足这种规范。
规范中规定尾数部分用纯小数给出,而且尾数的绝对值应大于或等于 1/R,并小于或等于 1,即小数点后的第一位不为零。
针对上面计算的结果,我们需要进行右规操作(尾数右移一位,阶码 +1):
->
0000011111010011001100110011001100110011001100110011001100110100
在规格化过程中,由于尾数的位数溢出所以移除了最低位,并进行了四舍五入的操作,同样操作了精度损失。
# 判断
根据阶码判断浮点运算是否溢出。而我们的阶码 0b00001111101 即不上溢,也不下溢。
接着我们可以经过转换得到它的二进制形式:
$1.0011001100110011001100110011001100110011001100110100 * 2 ** -2$
然后再转化得到十进制的结果为:0.30000000000000004440892098500626,显然不等于 0.3。
# 处理方式
最常见的解决方案就是设置一个误差范围值,通常称为“机器精度”,对 JavaScript 的数字来说,这个值通常是 $2^-52$。从 ES6 开始,该值定义在 Number.EPSILON 中。
另外,对于一些精度要求不高的计算,你也可以调用 round()
方法四舍五入或者 toFixed()
方法保留指定的位数。
或者,将小数转为整数再做计算,最后再将结果缩小之前放大的倍数。
function getDecimalLen(val) {
return (val.toString().split('.')[1] || '').length
}
function sum(num1, num2) {
const multiple = Math.max(getDecimalLen(num1), getDecimalLen(num2))
const expandPrecision = Math.pow(10, multiple)
const result = (num1 * expandPrecision + num2 * expandPrecision) / expandPrecision
return result
}
最后,你还可以借助一些第三方库,比如 bignumber.js (opens new window) 和 mathjs (opens new window) 来进行处理。
# 两个大数字相加
在 BigInt 出现之前,在 JavaScript 中无法处理超大整数相加的情形,但是我们可以通过一些方法来实现:
function bigSum(num1 = 0, num2 = 0) {
const valueArr1 = num1
.toString()
.split('')
.map(Number)
.reverse()
const valueArr2 = num2
.toString()
.split('')
.map(Number)
.reverse()
const resultLen = 1 + Math.max(valueArr1.length, valueArr2.length)
const resultArr = new Array(resultLen).fill(0)
let i = 0,
up = 0,
tmpItem = 0
valueArr1.forEach((item, index) => (resultArr[index] = item))
while (up || i < valueArr2.length) {
tmpItem = i < valueArr2.length ? valueArr2[i] + resultArr[i] + up : resultArr[i] + up
if (tmpItem > 9) {
resultArr[i] = tmpItem % 10
up = 1
} else {
resultArr[i] = tmpItem
up = 0
}
i++
}
return Number(resultArr.reverse().join(''))
}
当然,如今通过 BigInt 也能进行处理。
# 参考
← 渐进式 Web 应用 Ajax →