前言
本篇文章是《面试题一网打尽》专栏的 javascript 第二篇文章,彻底解决浮点数运算精度相关的面试题目。欢迎大家关注我的这个专栏。
一、IEEE 754 标准
我们经常在文档中看到这个标准感觉是什么高深的东西,其实 IEEE 是一个组织类似公司名称,754 就是一个编号而已,所以 IEEE754 就是这个组织提出的编号为 754 的规范文档,并不是什么高深的东西,这个文档我们可以在网上查看,也可以通过下面的网盘链接获取,这个文档的全程是《二进制浮点运算的IEEE754标准》
链接: https://pan.baidu.com/s/1wuKA_jE0cyn3X9w5WlSUKw 提取码: zh6t
1.1 IEEE754 规范
javascript 中整数和浮点数都是通过双精度浮点数的格式进行存储的,遵循IEEE754规范。下图截取自 IEEE754 规范的官方 pdf 文档
js 中应用的就是 double 的那一行,就是双精度,一共 11 + 53 = 64 位。
1.2 二进制的科学计数法
无论是整数还是浮点数,在存储之前都是需要先将十进制的数字转成二进制,再表示为科学记数法表示。二进制的科学记数法就是指数的底数为 2,比如
1.0001 * 2^10
转成科学记数法之后,一定是 1. 开头的,所以在存储的时候尾数部分【0001】存在后52位,但是实际上有一个永远是 1. 的开头,所以实际上是 53 为,这也是为什么最大的安全证书是 2^53 -1。
指数部分【10】存储在指数部分使用 11位存储,最后还有一位是符号位,0 表示正数,1表示负数。
一共就是 1 + 11 + 52 = 64 位。
1.3 转成二进制
(1)十进制的数字整数部分转成二进制的方法是【除二取余法】
(2)小数部分转成二进制的方法是【乘二取整法】
所以对于一个即有整数部分也有小数部分的浮点数,整数部分和小数部分就需要分开来计算,然后再合并。比如整数部分得到【1101】,小数部分得到【00101】,合并一块就是【1101.00101】然后再用科学记数法表示。
1.10100101 * 2^3
1.4 指数部分的偏移量
在存储的时候指数部分需要加上一个固定的偏移量 1023【十进制】,然后再转成二进制进行存储。
为什么需要这个偏移量,因为指数部分一共?11 位,指数位的取值范围是【 -1023 ~ 1023】
1 - 2^10 ~ 2^10 -1
即便是负指数再加上这个偏移量【1023】之后也会变成正数,这样指数部分就不用符号位来指定指数的符号了。
二、数字存储
2.1 整数
整数部分转换成二进制使用除 2 取余法,当整数可以被精确地表示为 64 位浮点数时,连续的整数值不会存在偏差,但是当整数的值太大,即大于 2^53 -1【9007199254740992】,整数转化成二进制就不止 53 位了,可能54,55位,需要截取 53 前 53 位存储。所以无法精确表示。
所以有 Number.MAX_SAFE_INTEGER 表示最大安全值,是 2^53 -1 超过这个安全值的整数,连续的整数值不能再用双精度浮点数一一对应表示了。
所有整数也都是用科学计数法存储的,比如
- 2^52 指数位是 52+1023的二进制形式, 小数位都是 0(因为是 2 的幂)
- 2^53 = 9007199254740992 二进制
- 能够被精确表示是指,转化成二进制后小于等于53位
对于大整数解决精度的问题,可以写个方法,通过转化成字符串的方式,模拟手工算术实现大数相加。
可以,使用Number.isInteger()判断数值是否是整数
2.2 对于浮点数
浮点数是指有小数位的数字。
浮点数转换成二进制,整数部分使用除 2 取余数,小数部分使用乘 2 取整的方法,转化成二进制101.000111....人后再把这个结果用使用科学计数法表示,1.01000111*2^2
IEEE 754标准定义了浮点数的二进制表示方法
- 分解:将浮点数分解为整数部分和小数部分
- 转换整数部分:使用?2取余法,假如结果是 10011
- 转换小数部分:使用??2取整法,无限循环小数取所需要的精度即可,假设结果是 00101
- 格式化:合并整数部分和小数部分 10011.00101,用科学计数法表示为指数形式1.001100101 * 2^4,【二进制数中的最高位1,并将其左侧的所有位移到小数点右边,形成一个介于1(含)和2(不含)之间的数。这个数称为尾数(Mantissa)】【小数点左移正指数,小数点右移负指数】
- 编码尾数:将尾数的小数部分和整数部分(整数部分一定是1,所以常被省略)【001100101】存在后52位中
- 编码指数:指数【4】,以特定的偏移量表示,在IEEE 754双精度浮点数中,偏移量是1023,所以实际指数存储的是,科学计数法的指数【4】 + 1023 = 1027 ,将1027转化为二进制的形式,存在11位的指数位
- 加上符号位,+0、 -1
有些小数使用乘2取整方法得到的结果是无限循环的值,所以在实际存储中只能截取其中的52位,就存在精度的问题。
对于浮点数的精度问题可以先转化成整数再做除法,对于浮点数的比较可以使用Number.EPSILON,计算两个浮点数的差值是否小于这个浮点数精度。
三、面试题
3.1 说说 js 为什么会存在数字精度丢失的问题,以及如何进行解决
- js 中的数字类型在内存中是按照 IEEE 754 规范中双精度浮点数存储的(共 64 位),(单精度浮点数32位)
- IEEE 754 是《二进制浮点数规范》其中 IEEE 代表一个公司,754 是一个规范的编号
- 64 = 1位符号位(0 代表整数,1代表负数) + 11 位指数位(k+1) + 52 位小数位 其实是 53 位,科学计数法的第一位永远是 1,也就是有效数总是 1.xxx 的形式,所以忽略不存了)
- 任何数字都可以使用科学计数法表示,在存储数字时是按照以 2 为基数科学计数法存储的,一个数值使用科学计数法表示之后,后 52 位只存储科学计数法的小数部分。所以能够存储的数值在一定范围内,有些数字使用科学计数法表示小数部分超过 52 位,就会精度丢失
- 可以使用?BigInt 来解决大整数相加,或者手动实现一个大数相加的方法。
3.2 为什么0.1 + 0.2 不等于 0.3?
- 这涉及到 JavaScript 使用 IEEE 754 标准来表示浮点数,而在该标准下,某些十进制小数无法完全准确表示为二进制小数,导致精度问题。
- 可以把小数*10转化成整数或者,使用 Number.EPSILON 比较差值
3.3 如何解决浮点数运算精度问题?
- 可以使用一些技巧,比如将浮点数转换为整数进行运算,然后再转换回浮点数。另外,可以使用库如 BigNumber.js 或者使用 ECMAScript 新引入的 Number.EPSILON 进行比较。
- 使用 toFixed(小数位数) 、toPrecision(整数+小数的总共的位数)
3.4 解释下 JavaScript 中的 Number.EPSILON。
- Number.EPSILON 是 JavaScript 中表示1与大于 1 的最小浮点数之差。它可以用于比较两个浮点数是否相等,例如 Math.abs(a - b) < Number.EPSILON。
3.5 如何在 JavaScript 中判断一个数字是整数?
- 可以使用 Number.isInteger() 方法,例如 Number.isInteger(5) 会返回 true,而 Number.isInteger(5.5) 会返回 false。
3.6 解释下 NaN(Not a Number)在浮点数运算中的作用。
- 当涉及到无效的浮点数运算时,JavaScript 会返回 NaN。NaN 是一种特殊的浮点数,表示一个无效或未定义的数值。
- 0/0
- Infinity/Infinity 、Infinity - Infinity
- 非数学运算,比如对负数开平方根、对负数取对数等
- 使用未初始化的变量进行运算
- 当阶码(指数部分)全为1时,只要位数不全为0 就是NaN
-
3.7 如何比较两个浮点数是否相等?
- 由于浮点数精度的问题,直接使用 == 或 === 可能会导致错误的比较结果。推荐使用 Math.abs(a - b) < Number.EPSILON 这种方式来比较浮点数的相等性。希腊字母
3.8 在 JavaScript 中如何处理大数运算?
- 对于大数运算,可以使用第三方库如 BigNumber.js,它提供了对大整数和大浮点数的支持。
- 自定义大数运算函数,
- 接受字符串形式的大数作为参数
- js特别大,或者特别小的数会用指数格式展示,所以在实现大数相加的时候参数要是字符串,
- 用toExponential 展示数字的指数表示形式