原因和背景
js中的number和精度丢失原因
在JavaScript中,所有的数字(包括整数和浮点数)都以64位双精度浮点数的形式存储,这是遵循IEEE 754标准的一部分。这个过程并不是在“编译时”发生的,因为JavaScript通常被认为是一种解释型语言,但它的行为可以类比为编译型语言中的编译过程。 以下是JavaScript处理数字的大致步骤:
- 解析(Parsing):当JavaScript代码被解析时,源代码中的十进制数字被识别并转换成抽象语法树(AST)中的数字字面量。
- 字面量评估(Literal Evaluation):在代码执行之前,这些数字字面量会被评估成它们的数值等价物。
- 存储(Storage):在JavaScript的执行环境中,所有的数字都以64位双精度浮点数的形式存储在内存中。这意味着,无论原始代码中的数字是整数还是浮点数,它们都会被转换成这种格式。
- 计算(Computation):在执行JavaScript代码时,所有的算术运算都是基于这种64位双精度浮点数格式进行的。
- 输出(Output):当数字被输出或显示时,它们通常被转换回十进制形式以供人类阅读。
这个过程是由JavaScript引擎(如V8引擎、SpiderMonkey等)自动管理的,开发者通常不需要关心这些底层细节。JavaScript引擎会处理从源代码中的十进制表示到内存中的二进制表示的转换,以及反向转换。
需要注意的是,由于这种转换,某些十进制小数无法精确地表示为二进制小数,这可能导致精度损失和一些奇怪的算术行为,比如0.1 + 0.2 !== 0.3。这是因为0.1和0.2的二进制表示是无限循环的,而计算机只能存储有限的位数,因此它们被近似表示。
在这个过程中,JavaScript中的数字(包括整数和小数)都是以64位双精度浮点数的形式处理和存储的。这意味着,当你在JavaScript代码中写一个数字时,它在内存中的表示形式是64位双精度浮点数,而不是直接作为十进制数存储.
64位双精度浮点数
64位双精度浮点数是一种浮点数的表示方式,它遵循IEEE 754标准。这种表示方式使用64位(即8字节)来存储浮点数,包括以下几个部分:
- 符号位(Sign bit) :1位,表示数值的正负,0表示正数,1表示负数。
- 指数位(Exponent bits) :11位,用来存储指数部分,采用偏移量(bias)的方式来表示实际的指数值。对于双精度浮点数,偏移量是1023(即2^10 - 1)。
- 尾数位(Mantissa or significand bits) :52位,用来存储尾数部分,即有效数字。在双精度浮点数中,尾数前面隐含了一个前导的1(对于规格化的数),因此实际能够表示的有效数字位数是53位。
JavaScript 使用的是 64 位双精度浮点数,它能够精确表示的数字是有限的。由于尾数位是52位,加上隐含的前导1,我们可以认为它有53位的有效数字。大约是15到17位十进制有效数字,超过这个范围的数字可能会失去精确性。
看源码前,预先了解:
科学计数法: 一种表示非常大或非常小的数字的方法,它将数字表示为一个1到10之间的数乘以10的幂。在JavaScript中,科学计数法通常用于表示超出Number类型正常范围的数字。
例如,数字123456789可以表示为科学计数法:
1.23456789e8
这里,
1.23456789是1到10之间的数,e8表示10的8次方。因此,1.23456789e8等于1.23456789 * 10^8。同样,非常小的数字也可以用科学计数法表示。例如,0.000000123可以表示为:
1.23e-7
这里,
1.23是1到10之间的数,e-7表示10的-7次方。因此,1.23e-7等于1.23 * 10^-7。在JavaScript中,当数字太大或太小时,它们会自动转换为科学计数法。例如:
let largeNumber = 12345678901234567890; console.log(largeNumber); // 输出:1.2345678901234568e+19 let smallNumber = 0.000000000123; console.log(smallNumber); // 输出:1.23e-10
np的实现思路
(1)辅助函数:用于取出待计算数字的小数部分长度(包含科学计数法的处理,用小数长度减去指数,就是真实的小数长度)
function digitLength(num: NumberType): number {
// Get digit length of e
const eSplit = num.toString().split(/[eE]/);
const len = (eSplit[0].split('.')[1] || '').length - +(eSplit[1] || 0);
return len > 0 ? len : 0;
}
(2)辅助函数:将number转化为15精度的number,通过toPrecision转字符串,再解析为浮点数
function strip(num: NumberType, precision = 15): number {
return +parseFloat(Number(num).toPrecision(precision));
}
(3)辅助函数:用于将科学计数法的number,转为一个没有小数部分的固定整数
function float2Fixed(num: NumberType): number {
if (num.toString().indexOf('e') === -1) {
return Number(num.toString().replace('.', ''));
}
const dLen = digitLength(num);
return dLen > 0 ? strip(Number(num) * Math.pow(10, dLen)) : Number(num);
}
(4)辅助函数 扩展能力 支持多个参数进行计算
function createOperation(operation: (n1: NumberType, n2: NumberType) => number): (...nums: NumberType[]) => number {
return (...nums: NumberType[]) => {
const [first, ...others] = nums;
return others.reduce((prev, next) => operation(prev, next), first) as number;
};
}
转为js:
function createOperation(operation) {
return function(...nums) {
const [first, ...others] = nums;
return others.reduce((prev, next) => operation(prev, next), first);
};
}
这段代码定义了一个名为
createOperation的函数,它接受一个参数operation,这个参数是一个函数,接受两个数字类型的参数(NumberType)并返回一个数字类型的结果(number)。createOperation返回一个新的函数,这个新函数接受任意数量的数字类型参数(...nums),并返回一个数字类型的结果。 工作原理:
- 首先,它将传入的参数数组
nums分解为第一个元素first和其余元素others。- 然后,它使用
reduce方法对others数组中的元素进行累加操作,累加的基础值是first。对于others中的每个元素next,它都会调用传入的operation函数,将当前的累加结果prev和next作为参数传递给operation,并更新累加结果。- 最终,
reduce方法返回的累加结果被返回,这个结果是一个数字。 这个函数的目的是创建一个新的函数,这个新函数可以将任意数量的数字通过传入的operation函数进行组合计算。
(5)加减计算,得到小数长度n,number* n*10(幂计算Math.pow),便可以转化为整数,计算后,再换算回来;Math.max用于指定小数长度较大的number,确保换算为整数
加法
const plus = createOperation((num1, num2) => {
// 取最大的小数位
const baseNum = Math.pow(10, Math.max(digitLength(num1), digitLength(num2)));
// 把小数都转为整数然后再计算
return (times(num1, baseNum) + times(num2, baseNum)) / baseNum;
});
减法
const minus = createOperation((num1, num2) => {
const baseNum = Math.pow(10, Math.max(digitLength(num1), digitLength(num2)));
return (times(num1, baseNum) - times(num2, baseNum)) / baseNum;
`});`
(6)乘除计算,依然是依靠小数长度,转为整数计算后,换算回来
乘法
const times = createOperation((num1, num2) => {
const num1Changed = float2Fixed(num1);
const num2Changed = float2Fixed(num2);
const baseNum = digitLength(num1) + digitLength(num2);
const leftValue = num1Changed * num2Changed;
checkBoundary(leftValue);
return leftValue / Math.pow(10, baseNum);
});
除法
const divide = createOperation((num1, num2) => {
const num1Changed = float2Fixed(num1);
const num2Changed = float2Fixed(num2);
checkBoundary(num1Changed);
checkBoundary(num2Changed);
// fix: 类似 10 ** -4 为 0.00009999999999999999,strip 修正
return times(num1Changed / num2Changed, strip(Math.pow(10, digitLength(num2) - digitLength(num1))));
});