资讯专栏INFORMATION COLUMN

你对Number一无所知

Hanks10100 / 635人阅读

摘要:表示正数,表示负数。是一个无符号整数,因为长度是位,取值范围是。浮点数具体数值的实际表示。例如对于单精度浮点数,指数部分实际最小值是,对应的尾数部分从一直到,相邻两小浮点数之间的距离都是而与最近的浮点数即最小的非规约数也是。

二进制表示小数

例如用二进制表示 0.8125

0.8125
0.8125*2 = 1.625 取整为 1
0.625*2=1.25 取整为 1
0.25*2=0.5 取整为 0
0.5*2=1 取整为 1
若是 *2 始终无法得到 1,就一直到位数用完,这也是浮点数并不精确的原因
即 0.8125 的二进制表示是 0.1101
即 0.8125 = 2^-1*1+2^-2*1+2^-3*0+2^-4*1
即 0.8125 = 0.5 + 0.25 + 0.0625

所以 0.1 到 0.9 的 9 个小数中,只有 0.5 可以用二进制精确表示

浮点数与定点数

在 JS 中,所有的数字都是基于 IEEE 754 的浮点数。除了浮点数,还有定点数,两者的区别就在于小数点的处理。同样是用64个bit表示一个数,定点数会用前 N 位来表示一个数的整数部分,用后 64 - N 来表示一个数的小数部分,这个 N 是固定的,对所有的数都是一样的。

64位浮点数

对于64位的浮点数,最高的1位是符号位 S,接着是11位的指数 E,剩下的52位为有效数字 M

S,Sign(1bit):表示浮点数是正数还是负数。0表示正数,1表示负数。

E,Exponent(11bit):指数部分。类似于科学记数法的 M*10^N 中的 N,只不过不是以10为底,而是以2为底。E是一个无符号整数,因为长度是11位,取值范围是 0~2047。但是科学计数法中的指数是可以为负数的,所以再减去一个中间数 1023,[0,1022]表示为负,[1024,2047] 表示为正

M,Mantissa(52bit):基数部分。浮点数具体数值的实际表示。

所以计算机将 5.8125 存储为浮点数的过程如下

用二进制表示5.8125,得到 101.1101

用类似科学计数法表示二进制数 101.1101,得到 1.011101 * 2 ^ 2

偏移指数部分,在 5.8125 中,指数为2,是正数,但实际上指数也可能为负数,例如如果是0.8125,指数就为-1了。11位的指数部分,为了包括负数,所有需要偏移 2^10-1,即 1023。所以指数偏移后为1025=2+1023,用二进制表示 10000000001

处理整数部分,用类似科学计数法表示二进制数后,小数点前面总为1,可以将这个1忽略,这样可以增加表示数的范围

综上,5.8125 在计算机中的存储形式 0(1位) + 0111,1111,111(11位) + 011101(6位) +0…0(46位)

M部分是二进制表示数,E控制小数点的位置,S控制数的正负

现在开始解释 Number 中的一些事

JS 中所有的数字都是浮点数,64位浮点数能准确表示的实数是有限的,例如之前提到, 0.1 到 0.9 的 9 个小数中,只有 0.5 可以用浮点数准确表示

盗图来源JavaScript浮点数陷阱及解法

也因为 JS 中所有的数字都是以浮点数的形式保存的,所以数字后面的“.”会被当作小数点来处理

所以有

var a = 0.42;        // 0.42
var b = .42;        // 0.42
42.toFixed( 3 );    // SyntaxError
(42).toFixed( 3 );    // "42.000"
0.42.toFixed( 3 );    // "0.420"
42..toFixed( 3 );    // "42.000"
Number.MAX_VALUE 是如何得来的

64位浮点数,M部分最大为52个1 + 被忽略的1,E部分最大为 2046-1023 = 1023,

为什么不是 2047-1023 = 1024,因为这个1024要用来表示Infinity

所以最大的二进制数位 1.1111…1(小数点后面52个1) * 2 ^ 1023

在计算机中存储形态为 0 | 111 1111 1110 | 111111...111(52个1)

let sum = 0
for (let i = 0; i < 53; i++) {
    sum = sum + Math.pow(2, 1023 - i)
    console.log(sum)
}
sum // 1.7976931348623157e+308
Number.MAX_VALUE // 1.7976931348623157e+308
// 如果把代码改成 i < 53,得到 1.7976931348623155e+308
Number.MIN_VALUE 是如何得来的

这个值是 最接近0且大于0的,且是精确表示的数

64位浮点数,M部分最小为0 + 被忽略的1,E部分最小为 0-1023 = -1023,

1*2^-1023,即二进制 0.00…001(1之前有1023个0)

Math.pow(2, -1023) // 1.1125369292536007e-308
Number.MIN_VALUE // 5e-324

嗯?WHY?

“如果浮点数的指数部分的编码值是0,尾数为非零,那么这个浮点数将被称为非规约形式的浮点数。IEEE 754标准规定:非规约形式的浮点数的指数偏移值比规约形式的浮点数的指数偏移值大1.例如,最小的规约形式的单精度浮点数的指数部分编码值为1,指数的实际值为-126;而非规约的单精度浮点数的指数域编码值为0,对应的指数实际值也是-126而不是-127。实际上非规约形式的浮点数仍然是有效可以使用的,只是它们的绝对值已经小于所有的规约浮点数的绝对值;即所有的非规约浮点数比规约浮点数更接近0。规约浮点数的尾数大于等于1且小于2,而非规约浮点数的尾数小于1且大于0.

...

除了规约浮点数,IEEE754-1985标准采用非规约浮点数,用来解决填补绝对值意义下最小规格数与零的距离。(举例说,正数下,最大的非规格数等于最小的规格数。而一个浮点数编码中,如果exponent=0,且尾数部分不为零,那么就按照非规约浮点数来解析)非规约浮点数源于70年代末IEEE浮点数标准化专业技术委员会酝酿浮点数二进制标准时,Intel公司对渐进式下溢出(gradual underflow)的力荐。当时十分流行的DEC VAX机的浮点数表示采用了突然式下溢出(abrupt underflow)。如果没有渐进式下溢出,那么0与绝对值最小的浮点数之间的距离(gap)将大于相邻的小浮点数之间的距离。例如单精度浮点数的绝对值最小的规约浮点数是,它与绝对值次小的规约浮点数之间的距离为。如果不采用渐进式下溢出,那么绝对值最小的规约浮点数与0的距离是相邻的小浮点数之间距离的倍!可以说是非常突然的下溢出到0。这种情况的一种糟糕后果是:两个不等的小浮点数X与Y相减,结果将是0.训练有素的数值分析人员可能会适应这种限制情况,但对于普通的程序员就很容易陷入错误了。采用了渐进式下溢出后将不会出现这种情况。例如对于单精度浮点数,指数部分实际最小值是(-126),对应的尾数部分从,一直到, 相邻两小浮点数之间的距离(gap)都是;而与0最近的浮点数(即最小的非规约数)也是。“

以上文字来源非规约形式的浮点数

总结来说,规定当浮点数的指数为允许的最小指数值,尾数不必是规范化的。

M部分最小为51个0和1个1

所以最小二进制数,0.00….001(小数点后面51个0)*2^-1022,即二进制0.00..001(1之前有52+1022个0)

Math.pow(2, -1074) // 5e-324
Number.MAX_SAFE_INTEGER

从 Number.MIN_SAFE_INTEGER 到 Number.MAX_SAFE_INTEGER 之间连续的整数都是可以准确表示的

E为52,算上偏移+1023,为1075,用二进制表示 10000110011

M为52个1,算上默认的1,为1.11….1(小数点后面52个1)

在计算机中存储形态为 0 | 100 0011 0011 | 111111...111(52个1)

整体来看就是 1.11…1(小数点后面52个1)*2^52,即 53个1

let sum = 0
for (let i = 0; i < 53; i++) {
    sum = sum + Math.pow(2, i)
}
sum // 9007199254740991
Number.MAX_SAFE_INTEGER // 9007199254740991
Number.MIN_SAFE_INTEGER

将 S 位表示为1

在计算机中存储形态为 1 | 100 0011 0011 | 111111...111(52个1)

0.1 + 0.2 === 0.3 // false

0.1 转成二进制 0.000110011001100...

即 1.1001100 * 2^-4

指数位偏移 1019 = -4+1023,用二进制表示 1111 1110 11

0.1 在计算机中存储形态 0 | 011 1111 1011|1001 1001 1001 1001 1001(12个1001) 1010

最后四位不是 1001 而是 1010,是考虑到 1001的下一位是1,故进一位

再将其转换成二进制 1.1001 1001 1001(12个1001)1010 * 2^-4

其实呢

0.1.toString(2) // "0.0001100110011001100110011001100110011001100110011001101"

0.2 转成二进制 0.0011001100110011...

即 1.1001 1001…*2^-3

指数位偏移 1020 = -3+1023,用二进制表示 1111 1111 00

0.2 在计算机中存储形态 0 | 011 1111 1100 | 1001 1001 1001 1001 1001(12个1001) 1010

最后四位不是 1001 而是 1010,是考虑到 1001的下一位是1,故进一位

再将其转换成二进制 1.1001 1001 1001(12个1001) 1010 *2^-3

0.2.toString(2) // "0.001100110011001100110011001100110011001100110011001101"

将这两个二进制数相加

写个小程序计算一下

function addBinary(a, b) {
    let aStr = a,
        bStr = b
    let addLength = aStr.length - bStr.length
    if (addLength > 0) {
        bStr = bStr + "0".repeat(addLength)
    } else if (addLength < 0) {
        aStr = aStr + "0".repeat(-addLength)
    }
    let addFlag = 0
    let arr1 = [...aStr]
    let arr2 = [...bStr]
    let length = arr1.length
    let arr3 = []
    for (let i = 0; i < length; i++) {
        let el1 = arr1.pop()
        let el2 = arr2.pop()
        if (el1 * 1 + el2 * 1 === 0) {
            arr3.unshift(addFlag)
            addFlag = 0
        } else if (el1 * 1 + el2 * 1 === 1) {
            if (addFlag === 1) {
                arr3.unshift(0)
                addFlag = 1
            } else {
                arr3.unshift(1)
                addFlag = 0
            }
        } else if (el1 * 1 + el2 * 1 === 2) {
            arr3.unshift(addFlag)
            addFlag = 1
        }
    }
    return arr3.join("")
}
// 参数a,b 为小数后面的部分
addBinary("0001100110011001100110011001100110011001100110011001101","001100110011001100110011001100110011001100110011001101")
// 0100110011001100110011001100110011001100110011001100111

加上整数部分,得到 0.0100110011001100110011001100110011001100110011001100111

将其转换为实数

function convertBinary(a) {
    return [...a].reduce((acc, cur, i) => {
        acc = acc + cur * Math.pow(2, -1 * (i + 1))
        return acc
    }, 0)
}
// 参数a 依然为小数后面的部分
convertBinary("0100110011001100110011001100110011001100110011001100111")
// 0.30000000000000004
Number.EPSILON
if (!Number.EPSILON) {
    Number.EPSILON = Math.pow(2,-52);
}

用 Number.EPSILON(容差) 来比较两个 number 的等价性

The value of Number.EPSILON is the difference between 1 and the smallest value greater than 1 that is representable as a Number value, which is approximately 2.2204460492503130808472633361816 x 10‍−‍16.

根据ECMASCRIPT-262定义 Number.EPSILON 是大于1的最小可表示数与1的差

function numbersCloseEnoughToEqual(n1,n2) {
    return Math.abs( n1 - n2 ) < Number.EPSILON;
}

var a = 0.1 + 0.2;
var b = 0.3;

numbersCloseEnoughToEqual( a, b );                    // true
numbersCloseEnoughToEqual( 0.0000001, 0.0000002 );    // false
Infinity, a + 1 === a

根据 IEEE 754

形式 指数 小数部分
0 0 0
非规约形式 0 非0
规约形式 1到2^e-2 任意
2^e-1 0
NaN 2^e-1 非0

如果指数是0并且尾数的小数部分是0,这个数±0(和符号位相关)

如果指数 =并且尾数的小数部分是0,这个数是±∞(同样和符号位相关)

如果指数 = 并且尾数的小数部分非0,这个数表示为不是一个数(NaN)

var a = Number.MAX_VALUE;    // 1.7976931348623157e+308
a + 1 === a;                // true
a + a;                        // Infinity
a + Math.pow( 2, 970 );        // Infinity
a + Math.pow( 2, 969 );        // 1.7976931348623157e+308

IEEE 754

Infinity 中 E的二进制表示为 111 1111 1111,M为1(默认) + 52个0

Infinity 在计算机中存储形态 0 | 111 1111 1111 | 0000(52个0)

Math.pow(2, 1024) // Infinity
Math.pow(2, 1023) + Math.pow(2, 1022) + ... + Math.pow(2, 971) // 1.7976931348623157e+308 

可以看到 InfinityNumber.MAX_VALUE 之间相差 Math.pow(2, 971)

IEEE 754 “就近舍入”,Number.MAX_VALUE + Math.pow( 2, 969 ) 比起 Infinity 更接近于 Number.MAX_VALUE,所以它“向下舍入”,而 Number.MAX_VALUE + Math.pow( 2, 970 ) 距离 Infinity 更近,所以它“向上舍入”。

再我的理解看来,凡是大于等于 Number.MAX_VALUE + Math.pow( 2, 970 ) 的数字都用 Infinity 来存储

存储形态都是 0 | 111 1111 1111 | 0000(52个0)

Number.MAX_VALUE + Math.pow(2,971) === Number.MAX_VALUE + Math.pow(2,972) // true
NaN

首先 typeof NaN 返回 number,NaN表示不是数字的数字

NaN 为数字表现为它来源于数学计算,

NaN 不是数字表现为计算过程中的参数并不符合要求,导致计算结果不是数字

"foo"/"foo" // NaN
1 * "fp" // NaN
1 / 0    // Infinity
0 / 0    // NaN
Infinity / Infinity // NaN
Infinity / 1 // Infinity
Infinity / 0 // Infinity

如何判断一个数值是 NaN

typeof n === "number" && window.isNaN(n)

n !== n

在整个语言中 NaN 是唯一一个自己与自己不相等的值

Number.isNaN(n)

0 & -0
Object.is(0, -0);            // false
Object.is(-0, -0);           // true
Object.is(NaN, 0/0);         // true
function fakeIs(v1, v2) {
    // 测试 `-0`
    if (v1 === 0 && v2 === 0) {
        return 1 / v1 === 1 / v2;
    }
    // 测试 `NaN`
    if (v1 !== v1) {
        return v2 !== v2;
    }
    // 其他情况
    return v1 === v2;
}
Number 的方法

numObj.toExponential(fractionDigits)

fractionDigits 规定了小数位的位数

以指数形式展现数字,科学计数法

numObj.toFixed(digits)

digits 规定了小数位的位数,不足用 0 填充

固定小数位数

numObj.toPrecision(precision)

precision 规定了整数位+小数位的位数

以固定精度返回数字

参考博客

Numbers in JavaScript

二进制与JS中的浮点值运算

你不懂JS/类型与文法/值

工具网站

二进制表示浮点数的在线转换

FloatConverter

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/93311.html

相关文章

  • python入门的正确姿势,从一无所知到无所不知

    摘要:不同的人的路线图版本会有所不同。寻找答案从一无所知到无所不知如果你在这个过程中多次遇到困难,在知难而退之前努力尝试解决问题。并不是成为一个全面的开发人员所需要的唯一技能。首先进行一两个月的学习阶段,然后进入一个月的构建阶段。 初级开发者学Python容易陷入茫然,面对市面上种类众多的编程语言和框架,重要的是坚持自己的选择,宜精不宜杂。本文是一篇指路文,概述了从编程基础、引导、文档阅读、...

    legendmohe 评论0 收藏0
  • 【译】确保网站性能的5个小贴士

    摘要:定期进行负载测试负载测试显示您的网站在一定数量的用户访问时的表现。如果负载测试显示的页面加载时间比预期的要长,那么网站设计的小改动就能带来所需的改进。 确保网站性能的5个小贴士 翻译:疯狂的技术宅作者:Jennifer Oksnevad英文标题:5 Tips to ensure website performance英文原文:https://www.catswhocode.com/b....

    ls0609 评论0 收藏0
  • 【译】确保网站性能的5个小贴士

    摘要:定期进行负载测试负载测试显示您的网站在一定数量的用户访问时的表现。如果负载测试显示的页面加载时间比预期的要长,那么网站设计的小改动就能带来所需的改进。 确保网站性能的5个小贴士 翻译:疯狂的技术宅作者:Jennifer Oksnevad英文标题:5 Tips to ensure website performance英文原文:https://www.catswhocode.com/b....

    singerye 评论0 收藏0
  • 可靠React组件设计的7个准则之封装

    摘要:组件可以处理其他组件的实例化为了避免破坏封装,请注意通过传递的内容。因此,将状态管理的父组件实例传递给子组件会破坏封装。让我们改进两个组件的结构和属性,以便恢复封装。组件的可重用性和可测试性显著增加。 翻译:刘小夕原文链接:https://dmitripavlutin.com/7-... 原文的篇幅非常长,不过内容太过于吸引我,还是忍不住要翻译出来。此篇文章对编写可重用和可维护的Re...

    yck 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<