Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

腾讯:js中精度问题及解决方案(一面) #34

Open
artdong opened this issue Feb 23, 2021 · 1 comment
Open

腾讯:js中精度问题及解决方案(一面) #34

artdong opened this issue Feb 23, 2021 · 1 comment

Comments

@artdong
Copy link

artdong commented Feb 23, 2021

面试公司:

腾讯

面试环节:

一面

问题:

遇到过js精度问题吗?是怎么解决的?

@artdong
Copy link
Author

artdong commented Feb 23, 2021

js精度问题

1.浮点数精度问题 2.大数精度问题

浮点数精度问题,比如 0.1 + 0.2 !== 0.3
大数精度问题,比如 9999 9999 9999 9999 == 1000 0000 0000 0000 1
toFixed 四舍五入结果不准确,比如 1.335.toFixed(2) == 1.33

四舍五入 toFixed()方法

let a = 2.446242342;
a = a.toFixed(2);  // 输出结果为 2.45
let b = 2.335;
b = b.toFixed(2);  // 输出结果为 2.33

round()、floor()、ceil() 等都不能真正的四舍五入,有精度问题。

ES6 在Number对象上面,新增一个极小的常量Number.EPSILON。它表示 1 与大于 1 的最小浮点数之间的差。
Number.EPSILON实际上是 JavaScript 能够表示的最小精度。误差如果小于这个值,就可以认为已经没有意义了,即不存在误差了。

round() 可以通过以下方式来确保精度是正确的:

let c = 2.446242342;
c = Math.round((c + Number.EPSILON) * 100) / 100;  // 输出结果为 2.45

let d = 2.335;
d = Math.round((d + Number.EPSILON) * 100) / 100;  // 输出结果为 2.34
// 能精确表示的整数范围上限,S为1个0,E为11个0,S为53个1
Math.pow(2, 53) - 1 === Number.MAX_SAFE_INTEGER    // true
// 能精确表示的整数范围下限,S为1个1,E为11个0,S为53个1
-(Math.pow(2, 53) - 1) === Number.MIN_SAFE_INTEGER    // true

[MIN_SAFE_INTEGER, MAX_SAFE_INTEGER] 的整数都可以精确表示,但是超出这个范围的整数就不一定能精确表示。这样就会产生所谓的大数精度丢失问题。

解决思路

首先考虑的是如何解决浮点数运算的精度问题,有 3 种思路:

1· 考虑到每次浮点数运算的偏差非常小(其实不然),可以对结果进行指定精度的四舍五入,比如可以parseFloat(result.toFixed(12));

2. 将浮点数转为整数运算,再对结果做除法。比如0.1 + 0.2,可以转化为(1 + 2)/10。 (转化后的两个整数相乘的结果有可能超过 MAX_SAFE_INTEGER)

3. 把浮点数转化为字符串,模拟实际运算的过程。

所以,最终考虑使用第三种方案,目前已经有了很多较为成熟的库,比如 bignumber.js,decimal.js,以及big.js等。
我们可以根据自己的需求来选择对应的工具。并且,这些库不仅解决了浮点数的运算精度问题,还支持了大数运算,并且修复了原生toFixed结果不准确的问题。

function toDecimal(num) {
    return Math.round(num + Number.EPSILON);
} 

以上方法虽然不会产生精度问题,但是它有一点小陷阱容易忽略。
toDecimal(-1.5) // -1 不是我们想要的结果

特殊精度解决方案

// 金额price(大于等于0)保留n(n一般为2)位小数,此方法不会有精度问题 

function formatPrice(price, digit) {
    const accuracyNum = Math.pow(10, digit);
    return parseFloat(Math.round((price + Number.EPSILON) * accuracyNum) / accuracyNum).toFixed(digit);
}

将浮点数转为整数运算,运算结果附上小数点 的四则运算,可能会出现数据溢出

//加
function add(arg1,arg2){ 
 let digits1,digits2,maxDigits; 
 try{digits1=arg1.toString().split(".")[1].length}catch(e){digits1=0} 
 try{digits2=arg2.toString().split(".")[1].length}catch(e){digits2=0} 
 maxDigits=Math.pow(10,Math.max(digits1,digits2)) 
 return (arg1*maxDigits+arg2*maxDigits)/maxDigits 
} 
  
//减
function sub(arg1,arg2){ 
 let digits1,digits2,maxDigits; 
 try{digits1=arg1.toString().split(".")[1].length}catch(e){digits1=0} 
 try{digits2=arg2.toString().split(".")[1].length}catch(e){digits2=0} 
 maxDigits=Math.pow(10,Math.max(digits1,digits2)); 
 return (arg1*maxDigits-arg2*maxDigits)/maxDigits; 
} 
 
//乘
function mul(arg1,arg2) { 
 let digits=0,s1=arg1.toString(),s2=arg2.toString(); 
 try{digits+=s1.split(".")[1].length}catch(e){} 
 try{digits+=s2.split(".")[1].length}catch(e){}
 return Number(s1.replace(".",""))*Number(s2.replace(".",""))/Math.pow(10,digits); 
}
 
//除
function div(arg1,arg2){ 
 let int1=0,int2=0,digits1,digits2; 
 try{digits1=arg1.toString().split(".")[1].length}catch(e){digits1=0} 
 try{digits2=arg2.toString().split(".")[1].length}catch(e){digits2=0} 
  
 int1=Number(arg1.toString().replace(".","")) 
 int2=Number(arg2.toString().replace(".","")) 
 return (int1/int2)*Math.pow(10,digits2-digits1); 
}

金额按照千分位格式化(不会有精度问题,不会有溢出问题)

const x= new BigNumber('123456789.123556789'); // 注意:参数必须为字符串,接口返回参数也必须为字符串

// 格式化(小数点)
console.log('-----x.toFormat()------', x.toFormat()); // '123,456,789.123556789'
console.log('-----x.toFormat(3)------', x.toFormat(2));  // '123,456,789.124'

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant