js toFixed
为什么(2.55).toFixed(1)等于2.5?
上次遇到了一个奇怪的问题:JS的(2.55).toFixed(1)输出是2.5,而不是四舍五入的2.6,这是为什么呢?
进一步观察:
发现,并不是所有的都不正常,1.55的四舍五入还是对的,为什么2.55、3.45就不对呢?
这个需要我们在源码里面找答案。
数字在V8里面的存储有两种类型,一种是小整数用Smi,另一种是除了小整数外的所有数,用HeapNumber,Smi是直接放在栈上的,而HeapNumber是需要new申请内存的,放在堆里面。我们可以简单地画一下堆和栈在内存的位置:
如下代码:
let obj = {};
这里定义了一个obj的变量,obj是一个指针,它是一个局部变量,是放在栈里面的。而大括号{}实例化了一个Object,这个Object需要占用的空间是在堆里申请的内存,obj指向了这个内存所在的位置。
栈和堆相比,栈的读取效率要比堆的高,因为栈里变量可以通过内存偏差得到变量的位置,如用函数入口地址减掉一个变量占用的空间(向低地址增长),就能得到那个变量在内存的内置,而堆需要通过指针寻址,所以堆要比栈慢(不过栈的可用空间要比堆小很多)。因此局部变量如指针、数字等占用空间较小的,通常是保存在栈里的。
对于以下代码:
let smi = 1;
smi是一个Number类型的数字。如果这种简单的数字也要放在堆里面,然后搞个指针指向它,那么是划不来的,无论是在存储空间或者读取效率上。所以V8搞了一个叫Smi的类,这个类是不会被实例化的,它的指针地址就是它存储的数字的值,而不是指向堆空间。因为指针本身就是一个整数,所以可以把它当成一个整数用,反过来,这个整数可以类型转化为Smi的实例指针,就可以调Smi类定义的函数了,如获取实际的整数值是多少。
如下源码的注释:
// Smi represents integer Numbers that can be stored in 31 bits.
// Smis are immediate which means they are NOT allocated in the heap.
// The this pointer has the following format: [31 bit signed int] 0
// For long smis it has the following format:
// [32 bit signed int] [31 bits zero padding] 0
// Smi stands for small integer.
在32位系统上使用一个int整型是32位,使用前面的31位表示整数的值(包括正负符号),而在64位系统上int整型是64位,使用前32位表示整数的值,所以在64位系统上减去一个符号位,还剩31位,所以Smi最大整数为:
2 ^ 31 - 1 = 2147483647 = 21亿
大概为21亿,而32位系统少一半。
到这里你可能会有一个问题,为什么要搞这么麻烦,不直接用基础类型如int整型来存就好了,还要搞一个Smi的类呢?这可能是因为V8里面对JS数据的表示都是继承于根类Object的(注意这里的Object不是JS的Object,JS的Object对应的是V8的JSObject),这样可以做一些通用的处理。所以小整数也要搞一个类,但是又不能实例化,所以就用了这样的方法——使用指针存储值。
大于21亿和小数是使用HeapNumber存储的,和JSObject一样,数据是存在堆里面的,HeapNumber存储的内容是一个双精度浮点数,即8个字节 = 2 words = 64位。关于双精度浮点数的存储结构我已经在《为什么0.1 + 0.2不等于0.3?》做了很详细的介绍。这里可以再简单地提一下,如源码的定义:
static const int kMantissaBits = 52;
static const int kExponentBits = 11;
64位里面,尾数占了52位,而指数用了11位,还有一位是符号位。当这个双精度的空间用于表示整数的时候,是用的52位尾数的空间,因为整数是能够用二进制精确表示的,所以52位尾数再加上隐藏的整数位的1(这个1是怎么来的可参考上一篇)能表示的最大值为2 ^ 53 - 1:
// ES6 section 20.1.2.6 Number.MAX_SAFE_INTEGER
const double kMaxSafeInteger = 9007199254740991.0; // 2^53-1
这是一个16位的整数,进而可以知道双精度浮点数的精确位数是15位,并且有90%的概率可以认为第16位是准确的。
这样我们就知道了,数在V8里面是怎么存储的。对于2.55使用的是双精度浮点数,把2.55的64位存储打印出来是这样的:
对于(2.55).toFixed(1),源码里面是这么进行的,首先把整数位2取出来,转成字符串,然后再把小数位取出来,根据参数指定的位数进行舍入,中间再拼个小数点,就得到了四舍五入的字符串结果。
整数部分怎么取呢?2.55的的尾数部分(加上隐藏的1)为数a:
1.01000110011...
它的指数位是1,所以把这个数左移一位就得到数b:
10.1000110011...
a原本是52位,左移1位就变成了53位的数,再把b右移52 - 1 = 51位就得到整数部分为二进制的10即十进制的2。再用b减掉10左移51位的值,就得到了小数部分。这个实际的计算过程是这样的:
// 尾数右移51位得到整数部分
uint64_t integrals = significand >> -exponent; // exponent = 1 - 52
// 尾数减掉整数部分得到小数部分
uint64_t fractionals = significand - (integrals << -exponent);
接下来的问题——整数怎么转成字符串呢?源代码如下所示:
static void FillDigits32(uint32_t number, Vector<char> buffer, int* length) {
int number_length = 0;
// We fill the digits in reverse order and exchange them afterwards.
while (number != 0) {
char digit = number % 10;
number /= 10;
buffer[(*length) + number_length] = '0' + digit;
number_length++;
}
// Exchange the digits.
int i = *length;
int j = *length + number_length - 1;
while (i < j) {
char tmp = buffer[i];
buffer[i] = buffer[j];
buffer[j] = tmp;
i++;
j--;
}
*length += number_length;
}
就是把这个数不断地模以10,就得到个位数digit,digit加上数字0的ascii编码就得到个位数的ascii码,它是一个char型的。在C/C++/Java/Mysql里面char是使用单引号表示的一种变量,用一个字节表示ascii符号,存储的实际值是它的ascii编码,所以可以和整数相互转换,如'0' + 1就得到'1'。每得到一个个位数,就除以10,相当十进制里面右移一位,然后继续处理下一个个位数,不断地把它放到char数组里面(注意C++里面的整型相除是会把小数舍去的,不会像JS那样)。
最后再把这个数组反转一下,因为上面处理后,个位数跑到前面去了。
小数部分是怎么转的呢?如下代码所示:
int point = -exponent; // exponent = -51
// fractional_count表示需要保留的小数位,toFixed(1)的话就为1
for (int i = 0; i < fractional_count; ++i) {
if (fractionals == 0)
break;
fractionals *= 5; // fractionals = fractionals * 10 / 2;
point--;
char digit = static_cast<char>(fractionals >> point);
buffer[*length] = '0' + digit;
(*length)++;
fractionals -= static_cast<uint64_t>(digit) << point;
}
// If the first bit after the point is set we have to round up.
if (((fractionals >> (point - 1)) & 1) == 1) {
RoundUp(buffer, length, decimal_point);
}
如果是toFixed(n)的话,那么会先把前n位小数转成字符串,然后再看n + 1位的值是需要进一位。
在把前n位小数转成字符串的时候,是先把小数位乘以10,然后再右移50 + 1 = 51位,就得到第1位小数(代码里面是乘以5,主要是为了避免溢出)。小数位乘以10之后,第1位小数就跑到整数位了,然后再右移原本的尾数的51位就把小数位给丢掉了,因为剩下的51位肯定是小数部分了,所以就得到了第一位小数。然后再减掉整数部分就得到去掉1位小数后剩下的小数部分,由于这里只循环了一次所以就跳出循环了。
接着判断是否需要四舍五入,它判断的条件是剩下的尾数的第1位是否为1,如果是的话就进1,否则就不处理。上面减掉第1位小数后还剩下0.05:
实际上存储的值并不是0.05,而是比0.05要小一点:
由于2.55不是精确表示的,而2.5是可以精确表示的,所以2.55 - 2.5就可以得到0.05存储的值。可以看到确实是比0.05小。
按照源码的判断,如果剩下的尾数第1位不是1就不进位,由于剩下的尾数第1位是0,所以不进位,因此就导致了(2.55).toFixed(1)输入结果是2.5.
根本原因在于2.55的存储要比实际存储小一点,导致0.05的第1位尾数不是1,所以就被舍掉了。
那怎么办呢?难道不能用toFixed了么?
知道原因后,我们可以做一个修正:
if (!Number.prototype._toFixed) {
Number.prototype._toFixed = Number.prototype.toFixed;
}
Number.prototype.toFixed = function(n) {
return (this + 3e-16)._toFixed(n);
};
就是把toFixed加一个很小的小数,这个小数经实验,只要3e-16就行了。这个可能会造成什么影响呢,会不会导致原本不该进位的进位了?我们刚刚提到双精度的精度是15位,第16位起是不可靠的,加上一个16位的小数可能会导致15位进1。但是如果两个数相差3e-16的话,其实几乎可以认为这两个数是相等的,所以加上这个造成的影响是可以忽略不计的。这个数和Number.EPSILON就差了一点点:
js toFixed的更多相关文章
- JS toFixed 四舍六入五成双
以前一直以为toFixed就是四舍五入的方法,后来又有一段时间以为toFixed是五舍六入.今天终于写的时候,终于才知道toFixed是一个叫做四舍六入无成双的诡异的方法... 完全不明白为什么要这么 ...
- js toFixed() 四舍五入后并不是你期望的结果
小学的时候学数学就知道有一种叫四舍五入的计算方式,就是对于小数位数的取舍,逢五进一,比如1.225 取两位小数后就是1.23.在前端开发中自己也少不了这样的计算,js也提供了相关的方法--toFixe ...
- 关于 js tofixed()保留小数位数问题
保留位数必须是数字 const num = parseFloat ('123456.33').tofixed(2); !!!! 注意 现在的的 num 是 字符串类型, 如果给它加数字的话,就会报错 ...
- js toFixed()方法的坑
javascript中toFixed使用的是银行家舍入规则. 银行家舍入:所谓银行家舍入法,其实质是一种四舍六入五取偶(又称四舍六入五留双)法. 简单来说就是:四舍六入五考虑,五后非零就进一,五后为零 ...
- js toFixed 方法重写,兼容负数
Number.prototype.toFixed = function (s) { var that = this, changenum, index; if (this < 0) { that ...
- JS精度损失toFixed
1234*0.01=12.3400000001 很明显后缀00001跟预期想要的不一致,起初面临这个问题我的处理方式是这样的: (1234*0.01).toString().substring(0,2 ...
- js小数计算小数点后显示多位小数(转)
首先写一个demo 重现问题,我使用的是一个js在线测试环境[打开] 改写displaynum()函数 function displaynum(){var num = 22.77;alert(num ...
- JS浮点计算精度问题分析与解决
问题描述 在JS计算四则运算时会遇到精度丢失的问题,会引起诸多问题,看看以下例子: 例如:在chrome控制台输入 0.1 + 0.7 输出结果是 0.7999999999999999 例如:0.1+ ...
- JS007. 深入探讨带浮点数运算丢失精度问题(二进制的浮点数存储方式)
复现与概述 当JS在进行浮点数运算时可能产生丢失精度的情况: 从肉眼可见的程度上观察,发生精度丢失的浮点数是没有规律的,但该浮点数丢失精度的问题会100%复现.经查阅,这个问题要追溯至浮点数的二进制存 ...
随机推荐
- python yield && scrapy yield
title: python yield && scrapy yield date: 2020-03-17 16:00:00 categories: python tags: 语法 yi ...
- TCP协议与UDP协议的区别以及与TCP/IP协议的联系
先介绍下什么是TCP,什么是UDP. 1. 什么是TCP? TCP(Transmission Control Protocol,传输控制协议)是面向连接的.可靠的字节流服务,也就是说,在收发数据前,必 ...
- Polya定理应用实例
关于Polya原理的应用经典实例: 问题:用两种颜色去染排成一个圈的6个棋子,如果通过旋转得到只算作一种.问有多少种染色状态. 解:先将棋子表上号: 1 6 2 5 3 4 那么把所有通过旋转 ...
- hdu5303贪心
http://acm.hdu.edu.cn/showproblem.php?pid=5303 说一下题目大意.. 有一个长为L的环..你家在原点位置0,那么剩下L-1个点上种有一些树, 给你树的位置和 ...
- Android 神奇的SpannableStringBuilder
一 无图言屌 先看看神奇的效果 仅用一个TextView实现 二 SpannableStringBuilder Google官方介绍 This is the class for text whose ...
- how to remove duplicates of an array by using js reduce function
how to remove duplicates of an array by using js reduce function ??? arr = ["a", ["b& ...
- MDN & JavaScript 文档翻译状态
MDN & JavaScript 文档翻译状态 https://developer.mozilla.org/zh-CN/docs/MDN/Doc_status/JavaScript refs ...
- skills share & free videos
skills share & free videos 技术分享 & 免费视频 https://www.infoq.cn/video/list WebAssembly https://w ...
- Flutter & UI system & GUI & API & SDK
Flutter & UI system & GUI & API & SDK https://book.flutterchina.club/chapter14/flutt ...
- Flutter 使用高德地图定位
amap_location 包 获取debug SHA1 // 使用debug.keystore获取debug SHA1 C:\Users\ajanuw\.android>keytool -li ...