javascript浮点数

javascript浮点数

js里面如果你去打印 console.info(0.1+0.2===0.3); 那必定是false,为什么呢?接下来一起看一下

references:

关于JavaScript中浮点数比较的问题

JavaScript 浮点数运算的精度问题

JavaScript如何判断参数为浮点型

Json.NET特殊处理64位长整型数据

JS中浮点数精度问题

如何避开JavaScript浮点数计算精度问题(如0.1+0.2!==0.3)

JavaScript 深入之浮点数精度

为什么JavaScript最大安全整数是2^53-1

javascript双精度浮点数

JavaScript 里的数字是采用IEEE 754 标准的 64 位双精度浮点数。该规范定义了浮点数的格式,对于64位的浮点数在内存中的表示,最高的1位是符号位,接着的11位是指数,剩下的52位为有效数字,具体:

  • 第0位:符号位, s 表示 ,0表示正数,1表示负数;
  • 第1位到第11位:储存指数部分, e 表示 ;
  • 第12位到第63位:储存小数部分(即有效数字),f 表示
    image

根据IEEE 754标准,任意一个二进制浮点数都可以表示成以下形式:

image

  • 符号位决定了一个数的正负,指数部分决定了数值的大小,小数部分决定了数值的精度。
  • IEEE 754规定,有效数字第一位默认总是1,不保存在64位浮点数之中。也就是说,有效数字总是1.xx…xx的形式,其中xx…xx的部分保存在64位浮点数之中,最长可能为52位。因此,JavaScript提供的有效数字最长为53个二进制位(64位浮点的后52位+有效数字第一位的1)。即(尾数部分M通常都是规格化表示的,即非"0"的尾数其第一位总是"1",而这一位也称隐藏位,因为存储时候这一位是会被省略的。比如保存1.0011时,只保存0011,等读取的时候才把第一位的1加上去,这样做相当于多保存了1位有效数字。)

真值表示方法:

image

最终表达式即为V=(-1)^S*2^(E-1023)*(M+1)

  • Q: 为什么指数要减去 1023
    • A: E 是一个无符号整数,能表示的最大值即为 111…1(11 个 1),第一位为符号位,此时表示的数即为 -1023-1023 因此将以 1023 为界限区分正负,或者另一种解释方式我们用 2047 个数表示正负值,自然要以中间分开。
  • Q: 为什么是 M+1
    • A: 因为所有的浮点数都可以表示为 1.xxxx * 2^xxx 的形式,前面的一定是 1.xxx,那干脆我们就不存储这个 1 了,直接存后面的 xxxxx 好了,这也就是 Fraction 的部分

== Number.MAX_VALUE ==

js 能表示的最大值,就要看1.x*2^y

y 最大值为 1023,x 为 52 个 1,则 Number.MAX_VALUE = Math.pow(2,1023)*(2-Math.pow(2,-52)) (1.11…1(52 个 1)即为 2-2^-52)

== Number.MAX_SAFE_INTEGER ==

对比数253与253+1:

2^53 我们尝试把它表示成二进制:1 53个0 ,规格化 1.0…00 * 2^53

那2^53+1呢?我们尝试把它表示成二进制:1 52个0 1 ,标准化 1.0…01 * 2^53

问题来了,尾数都有53位,但只要52个空! 它的处理办法是 忽略第53位 ,因此这两个数在计算机中表示的结果一样!

此时就不安全了。显而易见,在2^53-1之后的数中,只要指数相同,并且尾数前52位相同,则这个两个数数值相同。因此最大安全的数即为 Math.pow(2,53)-1

浮点数的精度问题

0.1+0.2 的过程是什么样的?
  • 首先,十进制的0.1和0.2都会被转换成二进制
  • 但由于浮点数用二进制表达时是无穷的,IEEE 754 标准的 64 位双精度浮点数的小数部分最多支持 52 位二进制位

最终

0.1 -> 0.0001100110011001...(无限)
0.2 -> 0.0011001100110011...(无限)

因浮点数小数位的限制而截断的二进制数字,再转换为十进制,就成了 0.30000000000000004。所以在进行算术计算时会产生误差。

具体计算过程可以参考:

如何避开JavaScript浮点数计算精度问题(如0.1+0.2!==0.3)

整数的精度问题

javaScript 中 Number类型统一按浮点数处理,整数是按最大54位来算最大(2^53 - 1,Number.MAX_SAFE_INTEGER,9007199254740991) 和最小(-(2^53 - 1),Number.MIN_SAFE_INTEGER,-9007199254740991) 安全整数范围的。所以只要超过这个范围,就会存在被舍去的精度问题。

因此也就出现了这样的问题

console.info(999928299928282828920===999928299928282828929);//true

当然这个问题并不只是在 Javascript 中才会出现,几乎所有的编程语言都采用了 IEEE-745 浮点数表示法,任何使用二进制浮点数的编程语言都会有这个问题,只不过在很多其他语言中已经封装好了方法来避免精度的问题,而 JavaScript 是一门弱类型的语言,从设计思想上就没有对浮点数有个严格的数据类型,所以精度误差的问题就显得格外突出。

一些补充:

1 字节=8位

int型4字节=32位

long long型为8字节=64位

那么16个二进制位能够表示多少种不同的整数呢?稍微用点数学常识就知道,是2的16次方,也就是65536个不同的整数。所以对于无符号整数,unsigned short的范围就是0~65535。

而为了表示负数,计算机用short的第一位作为符号位来表示正负。注意,计算机中是以补码的形式存放整数的。对于正数,补码是其本身;对于负数,其补码是对其绝对值的按位取反,再加1的结果。

解决精度问题

Number.EPSILON

表示1与大于1的最小浮点数之间的差.

对于64位浮点数来说,大于1的最小浮点数相当于二进制的1.00…001,小数点后面有连续51个零。这个值减去1之后,就等于2的-52次方.

console.info(Number.EPSILON===Math.pow(2,-52),Math.pow(2,-52));
// true 2.220446049250313e-16
function withinErrorMargin (left, right) {
  return Math.abs(left - right) < Number.EPSILON * Math.pow(2, 2);
}
// true
console.info(withinErrorMargin(0.1+0.2,0.3));

升阶

我们可以把需要计算的数字升级(乘以10的n次幂)成计算机能够精确识别的整数,等计算完成后再进行降级(除以10的n次幂),这是大部分变成语言处理精度问题常用的方法

/*** method **
 *  add / subtract / multiply /divide
 * floatObj.add(0.1, 0.2) >> 0.3
 * floatObj.multiply(19.9, 100) >> 1990
 *
 */
var floatObj = function() {
    /*
     * 判断obj是否为一个整数
     */
    function isInteger(obj) {
        return Math.floor(obj) === obj
    }
    /*
     * 将一个浮点数转成整数,返回整数和倍数。如 3.14 >> 314,倍数是 100
     * @param floatNum {number} 小数
     * @return {object}
     *   {times:100, num: 314}
     */
    function toInteger(floatNum) {
        var ret = {times: 1, num: 0}
        if (isInteger(floatNum)) {
            ret.num = floatNum
            return ret
        }
        var strfi  = floatNum + ''
        var dotPos = strfi.indexOf('.')
        var len    = strfi.substr(dotPos+1).length
        // 关键点
        var times  = Math.pow(10, len)
        // 关键点
        var intNum = Number(floatNum.toString().replace('.',''))
        ret.times  = times
        ret.num    = intNum
        return ret
    }
    /*
     * 核心方法,实现加减乘除运算,确保不丢失精度
     * 思路:把小数放大为整数(乘),进行算术运算,再缩小为小数(除)
     *
     * @param a {number} 运算数1
     * @param b {number} 运算数2
     * @param digits {number} 精度,保留的小数点数,比如 2, 即保留为两位小数
     * @param op {string} 运算类型,有加减乘除(add/subtract/multiply/divide)
     *
     */
    function operation(a, b, digits, op) {
        var o1 = toInteger(a)
        var o2 = toInteger(b)
        var n1 = o1.num
        var n2 = o2.num
        var t1 = o1.times
        var t2 = o2.times
        var max = t1 > t2 ? t1 : t2
        var result = null
        switch (op) {
            case 'add':
                if (t1 === t2) { // 两个小数位数相同
                    result = n1 + n2
                } else if (t1 > t2) { // o1 小数位 大于 o2
                    result = n1 + n2 * (t1 / t2)
                } else { // o1 小数位 小于 o2
                    result = n1 * (t2 / t1) + n2
                }
                return result / max
            case 'subtract':
                if (t1 === t2) {
                    result = n1 - n2
                } else if (t1 > t2) {
                    result = n1 - n2 * (t1 / t2)
                } else {
                    result = n1 * (t2 / t1) - n2
                }
                return result / max
            case 'multiply':
                result = (n1 * n2) / (t1 * t2)
                return result
            case 'divide':
                result = (n1 / n2) * (t2 / t1)
                return result
        }
    }
    // 加减乘除的四个接口
    function add(a, b, digits) {
        return operation(a, b, digits, 'add')
    }
    function subtract(a, b, digits) {
        return operation(a, b, digits, 'subtract')
    }
    function multiply(a, b, digits) {
        return operation(a, b, digits, 'multiply')
    }
    function divide(a, b, digits) {
        return operation(a, b, digits, 'divide')
    }
    // exports
    return {
        add: add,
        subtract: subtract,
        multiply: multiply,
        divide: divide
    }
}();

其他类库做补充

具体参考这篇文章:
JavaScript 浮点数运算的精度问题

常见问题

数据库用64位长整型存储数据的ID

数据库设计中常常会用bigint(64位)整数来作为主键,是一个非常重要而且不能有偏差的数据,比如,一条json数据

{"id":123456789012345678,"name":"James"}

传给前端就成了:

$.getJSON("/api/test").done(function(jo) {
    console.log(jo);
});
// Object {id: 123456789012345680, name: "James"}

出现了失真的情况;

解决方案:
可以把服务器端的64位整数处理成字符串类型。

public class HexLongConverter : JsonConverter
{
    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        // 由于CanConvert过滤,数据类型只可能是long或ulong
        // 统一转换成long类型处理
        long v = value is ulong ? (long)(ulong)value : (long)value;
        writer.WriteValue(v.ToString("X16"));
    }
    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        // 取得读到的十六进制字符串
        string hex = reader.Value as string;
        // 调用ToInt64扩展将字符串转换成long型
        // ToInt64扩展方法后附
        long v = hex.ToInt64(NumberStyles.HexNumber, 0L);
        // 将v转换成实际需要的类型 ulong 或 long(不转换)
        return typeof (ulong) == objectType ? (object) (ulong) v : v;
    }
    public override bool CanConvert(Type objectType)
    {
        // 只处理long和ulong两种类型的数据
        switch (objectType.FullName)
        {
            case "System.Int64":
            case "System.UInt64":
                return true;
            default:
                return false;
        }
    }
}

在序列化或反序列化模型的时候,只需要加入HexLongConverter对象作为参数即可:

// 序列化
string json = JsonConvert.SerializeObject(model, new HexLongConverter());
// 反序列化
SomeModal model = JsonConvert.DeserializeObject<model>(json, new HexLongConverter));

两个大整数相加

    const bigNumber=(str1,str2)=>{
        let arr1=str1.split('');
        let arr2=str2.split('');
        let res=[];
        let c=0;
        while(arr1.length||arr2.length){
            let a=arr1.pop()||0;
            let b=arr2.pop()||0;
            let temp=parseInt(a)+parseInt(b)+c;
            if (temp>=10){
                c=1;
                res.unshift(temp%10);
            }else{
                c=0;
                res.unshift(temp);
            }
        }
        return res.join('');
    };
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值