w3ctech

Javascript 中小数和大整数的精度丢失问题(具体讨论过程见群里)

事情的起因是昨天在群里(什么群?还没有加入 W3ctech交流群 你就OUT了,群号:175603092)和 @丷悠飏♬♩♪♫@1900. 发生的激烈讨论,有段时间没有这么好的感觉了,哈哈,又学到了新知识,所以,在此和大家分享一下我们讨论的成果,也希望大家参与其中,发掘更多的技术,毕竟 探索 是咱们程序员必备技能之一嘛!

事情的起因:

起因源于jQuery的两个函数 data()attr() 对于 DOM 数据的存储和读取,一个是隐性的设置和读取DOM的属性名和属性值,一个是设置和读取HTML标签设置的属性,我在这里简单的引用一下:

data()

Store arbitrary data associated with the matched elements or return the value at the named data store for the first element in the set of matched elements. The .data() method allows us to attach data of any type to DOM elements in a way that is safe from circular references and therefore from memory leaks.

attr()

Get the value of an attribute for the first element in the set of matched elements or set one or more attributes for every matched element. The .attr() method gets the attribute value for only the first element in the matched set. To get the value for each element individually, use a looping construct such as jQuery's .each() or .map() method.

更详细的解释可以去jQuery的官方文档去查看。这本来是无可厚非的两个方法,不过问题这就来了,看下面的代码:

$('#A').data('id');
// output: 10150104320000128
$('#A').attr('data-id');
// output: 10150104320000127

HTML代码应该是这样的:

<div id="A" data-id="10150104320000127"></div>

一开始我以为 @1900.data 设置了一个值,所以会导致两次输出结果不一样,可事实并非如此,就简单的HTML代码,然后调用这两个函数就会出现不同的结果,WHY?然后 @丷悠飏♬♩♪♫ 提出了是 Javascript大数损失精度 的问题,并给出了链接。身为小伙伴的我瞬间就震惊了有没有,不过看完这篇 blog 后,学到很多东西,同时深感自己学识的匮乏,幸好毕业没多长时间,《计算机组成原理》的一些东西还没忘干净。。。下面我就做一下简单的梳理:

  • 首先在 div 标签上设置了 data-id 属性;
  • 然后调用 data 函数读取 id 属性值;(这里我要解释一下,自从HTML5出来以后,对这种非标准的数据属性给出了一个解决方案,就是给Element对象设置一个 dataset 属性,任何以“data-”为前缀的小写属性名字都是合法的,所以自从jQuery1.4.3版本以后HTML 5 data- attributes在jQuery封装对象的时候都会自动放入jQuery的 data对象,所以用data()函数才能读到值,所以才会发生数据转换。)
  • 由于data-id的字符串值转换成了 Number,而且符合大数丢失精度的条件,所以精度丢失,最后输出的是丢失精度的值。

我记得在看《权威指南》的时候,看到过一个关于精确度的例子:

var x = .3 - .2;
var y = .2 - .1;
x == y      // => false:两值不相等

书中还明确指出:在任何使用二进制浮点数的编程语言中都会有这样的问题。只是感觉很奇怪,但是没有细想,只怪当时太年轻 T.T 。大家可以先看一下这篇博客,那么关键就在于二进制双精度浮点数的存储方式:s x m x 2^es 是符号位, m 是尾数,有52bits,e 是指数,有11bits,关键点就在那个 尾数 m,大家可以在博客中看到Number.MAX_VALUE是一个很大的值,而这个很大的值是相对于那个 指数e 说的,实际存储的精度还得看 尾数 m,而尾数m只有52位,所以当 x 小于等于 2^53 时可以确保x的精度不会丢失。当 x 大于2^53时,x 的精度有可能会丢失,那么这里为什么是小于等于 2^53,而不是小于 2^53 呢,原因就在于 隐藏位(hidden bit),博客中也有提到,当 指数e的二进制位全为0 时,隐藏位为0,如果 不全为0,则隐藏位为1,这应该是基于指数表达式的存储方式决定的,隐藏位也就是指数的底数里面的 整数部分,尾数 m则是指数中底数的 fraction小数部分。博客中也给出了隐藏位的参考链接,如果想深入学习的话,可以看看。

那面对精度的丢失是否解决方案呢,答案是有的,比如 Java 就进行了数据类型的封装,网上一般给出的方法是将数据转成字符串,进行分割,分别计算,比如小数的计算:

function numAdd(num1[String], num2[String]) { 
    var baseNum, baseNum1, baseNum2; 
    try { 
        baseNum1 = num1.split(".")[1].length; 
    } catch (e) { 
        baseNum1 = 0; 
    } 
    try { 
        baseNum2 = num2.split(".")[1].length; 
    } catch (e) { 
        baseNum2 = 0;
    } 
    baseNum = Math.pow(10, Math.max(baseNum1, baseNum2)); 
    return (num1 * baseNum + num2 * baseNum) / baseNum; 
};

当然实际的生产环境不会用这么简单的方法,其中应该先设计大整数的运算方法,再在小数运算中引用,返回值也应该是字符串,这个待我再研究研究,希望大家也能提提意见啦!

Nokey

Nokey

w3ctech微信

扫码关注w3ctech微信公众号

共收到2条回复

  • 简化的描述,应该是10150104320000127参与运算时,由于需要用双精度浮点表示,1mMath.pow(e),由于m只有52位,只能用Math.pow(2, 52)* Math.pow(2, e)来表示,导致原本的二进制位数部分53之后的部分被丢弃,精度就差啦。

    回复此楼
  • @步天天天 哈哈,可以这么理解,而且大于 2^53 的数做运算,它的个位必是偶数。

    var a = 10150104320000127;
    a+1;        // => 10150104320000128
    a+2;        // => 10150104320000130
    
    回复此楼