Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

如何优雅地取数值的整数和小数部分 #5

Open
akira-cn opened this issue Jul 25, 2019 · 15 comments
Open

如何优雅地取数值的整数和小数部分 #5

akira-cn opened this issue Jul 25, 2019 · 15 comments

Comments

@akira-cn
Copy link
Owner

akira-cn commented Jul 25, 2019

在处理数值的时候,获取浮点数的整数和小数部分,是一种常见的操作,在JavaScript中有许多方法可以达到目的,但也正因为方法众多,所以哪种方法更好,也值得我们仔细研究一番。

取整数

parseInt比较常用来取整数部分,在一些项目中经常能看到:

let num = 3.75;
console.log(parseInt(num)); // 3
num = -3.75;
console.log(parseInt(num)); // -3

用parseInt取整数,一般情况下,结果是没问题的,但是如果严格来说,其实parseInt并不是设计用来取整数的。

👉🏻 知识点parseInt(string, radix) 这个方法是一个将字符串转换为整数的方法,它有两个参数,第一个参数表示要转换的字符串,如果参数不是一个字符串,则将其转换为字符串。第二个参数是基数即进制,默认为10。

所以实际上parseInt(3.75)这个代码,会先将3.75转为字符串"3.75",然后再将它parseInt成为3。

所以用parseInt方法取整数,有两个不好的地方,一是parseInt这个函数名,看起来就是将字符串转整数的,用在这里不是很适合,另一个是转字符串有点多此一举,而且肯定会带来性能开销,所以使用parseInt虽然方便,但不是最好的办法。

💡补充

这个toString不仅仅是“多此一举”,还可能导致严重的问题,比如:

console.log(parseInt(0.00000001));  // 1
console.log(parseInt(1000000000000000000000)); // 1

这是因为,0.00000001.toString() === 1e-81000000000000000000000..toString() === 1e+21

既然parseInt不好用,有经验的同学,会想到用Math的方法来取整,相关的有3个方法,分别是Math.ceil、Math.round和Math.floor。

其中Math.round是四舍五入的,Math.ceil是向上取整,Math.floor是向下取整。

要达到parseInt的结果,我们需要判断数值的符号,如果是负数,要使用Math.ceil,如果是正数,则使用Math.floor:

function trunc(num) {
  if(num >= 0) return Math.floor(num);
  return Math.ceil(num);
}

console.log(trunc(3.75)); // 3
console.log(trunc(-3.75)); // -3

使用Math.round和Math.ceil实现trunc方法,要比使用parseInt的性能好,因为省去了转字符串。我们可以用jsperf测一下:

结果如下图:

看到使用Math.floor+Math.ceil明显要快。

实际上,在ES2015之后,还提供了原生的Math.trunc,我们可以更方便地使用Math.trunc,不用自己使用Math.floor和Math.ceil去实现了:

console.log(Math.trunc(3.75)); // 3
console.log(Math.trunc(-3.75)); // -3

tricky

如果看一些库的代码,你可能会看到这样的取整方式:

let num = 3.75;
console.log(num | 0); // 3
num = -num;
console.log(num | 0); // -3

这是一种利用位或“|”操作来取整的手段,老司机经常用,我以前也用。

位或运算为什么能达到我们的效果呢,具体可以看ECMA-262文档

对位操作的处理中,第5、6步,会把操作数转为Int32,所以我们就可以利用这个特点来使用“|”操作符了。

不过这么做也是有缺陷的,你发现问题了吗?

👉🏻 冷知识:因为bitwise操作将操作数转为Int32,所以它不能处理超过32位的数值取整,而JavaScript有效整数的范围是53位。

const num = 17179869184.89;
console.log(num | 0); // 0
console.log(Math.trunc(num)); // 17179869184

那么用“|”有什么好处呢?如果考虑js文件大小,那么a|0与其他方式比较,是最短的方式,所以如果要考虑压缩代码的大小,且明确知道数值范围不会超过32位整数的时候,可以考虑使用这个技巧。

取小数

取了整数部分,接下来取小数部分就很简单了:

function fract(num) {
  return num - Math.trunc(num);
}

console.log(fract(3.75)); // 0.75
console.log(fract(-3.75)); // -0.75

上面的代码思路就是先用Math.trunc(num)取整,然后再与原数相减,就得到了小数部分。

但是,我们还有更加简单的办法:

👉🏻 知识点:JavaScript的取模运算%并不限于整数运算,可以对浮点数取模。

所以,直接将原数对1取模,即可获得小数部分!

console.log(3.75 % 1); // 0.75
console.log(-3.75 % 1); // -0.75

这是最简单的取小数的方式,然后反过来,还可以倒推出另一种实现trunc取整的方式:

function trunc(num) {
  return num - num % 1;
}

扩展

取小数部分,可以用来实现周期函数,比如实现匀速的js周期动画:

<div id="progress_bar"></div>
#progress_bar {
  display: inline-block;
  width: 0px;
  height: 20px;
  background: red;
}
function run(el, duration) {
  const startTime = Date.now();

  function update() {
    let p = (Date.now() - startTime) / duration;
    p %= 1;
    el.style.width = `${300 * p}px`;
    requestAnimationFrame(update);
  }
  update();
}

const bar = document.getElementById('progress_bar');
run(bar, 3000);

如果我们的周期函数要考虑负数那一半区间,其实fract的方式要修改一下:

function fract(num) {
  return num - Math.floor(num);
}

这个方式才是正确的周期,它和之前的实现区别是负数区间返回的值不同,前者负数返回的小数部分为负数,这个实现中,如果num是正数,返回num的小数部分,如果num是负数,返回1.0 + num的负数小数部分,这样就保证返回值始终在0.0~1.0的区间内。

function fract(num) {
  return num - Math.floor(num);
}
console.log(-3.75 % 1); // -0.75
console.log(fract(num)); // 0.25

好了,关于取整和取小数的讨论就到这里。如果你们还有哪些关于取整和取小数的问题,欢迎在issue中讨论。

@guowenfh
Copy link

guowenfh commented Jul 26, 2019

~~1.233434 // 1
~~-2.75 // -2
~~-0 // 0

@qgy18
Copy link

qgy18 commented Jul 26, 2019

parseInt(0.0000001); // 1,这也是一个常见的坑

parseInt("0.0000001"); // 0
0.0000001.toString(); // "1e-7"

@akira-cn
Copy link
Owner Author

嗯嗯,@qgy18 这个我给加上了~

@akira-cn
Copy link
Owner Author

~~1.233434 // 1
~~-2.75 // -2
~~-0 // 0

这个也是利用位操作转Int32,和|0的原理一样

@hax
Copy link

hax commented Jul 26, 2019

如果考虑js文件大小,那么a|0与其他方式比较,是最短的方式,所以如果要考虑压缩代码的大小,且明确知道数值范围不会超过32位整数的时候,可以考虑使用这个技巧。

这个真节约不了几个字节哈……想节约还不如换个更好的gzip压缩算法或上Brotli压缩。或者看好图片之类的资源文件,随随便便几百K甚至几M就出去了。这就好比每天跟卖菜小贩讨价还价1个小时节约1毛,买房时不知道多找几个中介,便宜一个点就是几万。😂

我个人认为我们只应该在确定要进行 int32 转型的时候用它,或者按asm.js的约定以 x = x|0 作为类型标注。手动优化而丢失代码逻辑原本的intention是导致可维护性下降的万恶之源。(比如超出32bit或特殊值如Infinity、NaN就统统静悄悄变成了0,而出问题的时候你不知道0到底是从哪里来的 🙃)

@hrone
Copy link

hrone commented Jul 26, 2019

JavaScript的取模运算%并不限于整数运算,可以对浮点数取模。
但是只要是浮点数计算,无论加减乘除无一例外会出现经度丢失问题。
let num = 3.22;
console.log(num % 1); //0.2200000000000002
所以可以考虑字符串的split的方法,就是麻烦点。

@akira-cn
Copy link
Owner Author

如果考虑js文件大小,那么a|0与其他方式比较,是最短的方式,所以如果要考虑压缩代码的大小,且明确知道数值范围不会超过32位整数的时候,可以考虑使用这个技巧。

这个真节约不了几个字节哈……想节约还不如换个更好的gzip压缩算法或上Brotli压缩。或者看好图片之类的资源文件,随随便便几百K甚至几M就出去了。这就好比每天跟卖菜小贩讨价还价1个小时节约1毛,买房时不知道多找几个中介,便宜一个点就是几万。😂

我个人认为我们只应该在确定要进行 int32 转型的时候用它,或者按asm.js的约定以 x = x|0 作为类型标注。手动优化而丢失代码逻辑原本的intention是导致可维护性下降的万恶之源。(比如超出32bit或特殊值如Infinity、NaN就统统静悄悄变成了0,而出问题的时候你不知道0到底是从哪里来的 🙃)

是的,所以现在基本上不推荐使用了,在以前jQuery那个年代,这个用法还挺多见的

@akira-cn
Copy link
Owner Author

JavaScript的取模运算%并不限于整数运算,可以对浮点数取模。
但是只要是浮点数计算,无论加减乘除无一例外会出现经度丢失问题。
let num = 3.22;
console.log(num % 1); //0.2200000000000002
所以可以考虑字符串的split的方法,就是麻烦点。

如果取小数部分是为了做浮点数运算,精度问题是没关系的,如果是为了显示的话,那么应该转字符串,用split也好,用正则表达式也好,都可以的。

@hax
Copy link

hax commented Jul 26, 2019

理论上说这里不存在浮点数『精度』的问题,因为在机器内部本来就不是十进制。长远说我们要等专门的decimal类型(bigint都有了,decimal还会远吗?——还真不好说),短期就得自己走字符串转换下,我一般用 function roundFloat(x, p = 15) { return +x.toPrecision(p) } roundFloat(3.22%1) 返回 0.22。(用split、正则等的问题你不但需要处理一串0跟着零头,也可能是一串9跟着零头,比如 3.21%1)

@akira-cn
Copy link
Owner Author

如果整数和小数要分开显示的话,我一般是直接用toPrecision或toFixed先将它转字符串,然后split或正则分别取整数和小数部分,这样也不需要处理零头。

@fengyun2
Copy link

@akira-cn toFixed转换为字符串的时候就已经失去精度了吧?

@akira-cn
Copy link
Owner Author

@akira-cn toFixed转换为字符串的时候就已经失去精度了吧?

这跟失去精度没关系,toFixed转字符串是为了格式化显示,正常的运算是没问题的,只是浮点数比较不能用===和!==,应该要用下面这个方法:

function floatEqual(num, dest) {
    return Math.abs(num - dest) < Number.EPSILON;
}
const num = 0.2 + 0.4;
console.log(num, floatEqual(num, 0.6)); // 0.6000000000000001, true

@azl397985856
Copy link

取整数

const num = 3.11;
num >> 0 // 3
-1 * num >> 0 // -3

@akira-cn
Copy link
Owner Author

取整数

const num = 3.11;
num >> 0 // 3
-1 * num >> 0 // -3

num >> 0num | 0一样,都只能取Int32整数,而且比num | 0还多一个字节。

const num = 4294967296.99;
num >> 0; // 0

非要用的话用 num >>> 0 好一点:

const num = 2147483648.88;
num >> 0; // -2147483648
num >>> 0; // 2147483648

@q269384828
Copy link

JavaScript的取模运算%并不限于整数运算,可以对浮点数取模。
但是只要是浮点数计算,无论加减乘除无一例外会出现经度丢失问题。
let num = 3.22;
console.log(num % 1); //0.2200000000000002
所以可以考虑字符串的split的方法,就是麻烦点。

但是 num-num%1 取到的整数是对的

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

8 participants