双重按位非运算符 ~~ 对数字取整
介绍
按位非运算符(~
)将操作数的位反转。它将操作数转化为 32 位的有符号整型。也就是可以对数字进行取整操作(保留整数部分,舍弃小数部分)。
~-2 // 1
~-2.222 // 1
并且按位非运算时,任何数字 x
(已被转化为 32 位有符号整型) 的运算结果都是 -(x + 1)
。
那么双重按位非(~~
)对数字的运算结果就是 -(-(x + 1) + 1)
,结果就是 x
。
所以利用 ~~
操作数字时就可对其进行取整操作(右移操作符 x >> 0
和按位或操作符 x | 0
也有相同作用)。
如果操作的不是 Number
类型的,操作的对象会先转化 Number
类型,下面一起来看看。
操作原始数据类型时
~~(-2.999); // => -2
~~null; // => 0
~~undefined; // => 0
~~0; // => 0
~~(1/0); // => 0
~~false; // => 0
~~true; // => 1
~~'1234' // => 1234
~~'1234asdf' // => 0
~~NaN // => 0
~~
对于不能转化为数字的数据(NaN
) ,操作的结果为 0
右移操作符 >>
和按位或操作符 |
也是如此。
(-2.999) >> 0 // => -2
null >> 0 // => 0
undefined >> 0 // => 0
0 >> 0 // => 0
(1/0) >> 0 // => 0
false >> 0 // => 0
true >> 0 // => 1
'1234' >> 0 // => 1234
'1234asdf' >> 0 // => 0
NaN >> 0 // => 0
(-2.999) | 0 // => -2
null | 0 // => 0
undefined | 0 // => 0
0 | 0 // => 0
(1/0) | 0 // => 0
false | 0 // => 0
true | 0 // => 1
'1234' | 0 // => 1234
'1234asdf' | 0 // => 0
NaN | 0 // => 0
操作对象数据类型时
当 ~~
作用于对象类型时,对象类型会先隐式转化为数字,转化的结果取决于对象的 valueOf
方法和 toString
方法返回的结果。如果对象类型转化后最终的结果是 NaN
,那么 ~~
操作 NaN
则会直接返回 0
。
详细的转换过程:
调用对象的
valueOf
方法:
如果该方法返回一个原始值,JavaScript会尝试将这个原始值转换为一个数字。如果valueOf
方法返回的还是一个对象,JavaScript会继续调用对象的toString
方法。调用对象的
toString
方法:
这个方法返回一个字符串,然后JavaScript会尝试将这个字符串转换为一个数字。转换字符串为数字:
一旦从valueOf
或toString
方法中获得了一个原始值(通常是字符串),JavaScript会按照字符串到数字的转换规则来处理这个值。
如果valueOf
或toString
返回的值是NaN
,那么结果就是NaN
。如果字符串不能被解析为一个有效的数字,结果也是NaN
。 ~~
操作 NaN
返回 0
。
所以也就有下面的结果:
~~{}; // => 0
~~{a:1} // => 0
~~[]; // => 0
~~[1]; // => 1
~~[1,2]; // => 0
对于数组而言,将数组转化为数字,会调用数组的 toString()
。
数组的
toString
方法实际上在内部调用了join()
方法来拼接数组并返回一个包含所有数组元素的字符串,元素之间用逗号分隔。如果join
方法不可用或者不是函数,则会使用Object.prototype.toString
来代替,并返回[object Array]
。
上面的 [1,2] 经过 toString()
后是 '1,2'
, 转为数字则是 NaN
。所以 ~~[1,2]
结果为 0。
下面是对象有自定义的 valueOf()
或者 toString()
情况
var a = {
valueOf:function(){return '11'}, // 字符串'11' 可被转化为 数字 11
toString:function(){return 12}
}
~~a // => 11
var b = {
valueOf:function(){return 'asdf'}, // 字符串'asdf' 转化为 NaN
toString:function(){return 12}
}
~~b // => 0
var c = {
toString:function(){return 12} // 没有 valueOf() ,则调用 toString()
}
~~c // => 12
var d = {
toString:function(){return 'asdf'} // 字符串'asdf' 转化为 NaN
}
~~d // => 0
可进行运算的数字的有效范围
由于 按位运算总是将操作数转换为 32 位整数。 超过 32 位的数字将丢弃其最高有效位。如下例子中(来自MDN),超过 32 位的整数将转换为 32 位整数:
Before: 11100110111110100000000000000110000000000001
After: 10100000000000000110000000000001
再比如 ~~
操作日期类型数据,Date
的 valueOf
方法返回以数值格式表示的一个 Date
对象的原始值,从 1970 年 1 月 1 日 0 时 0 分 0 秒到该日期对象所代表时间的毫秒数。
返回的毫秒数是超过 32 位的整数,不在 ~~
操作的有效范围内,结果就不会是期望的那样。
var date = new Date()
Number(date) // 1706671595364
~~date // 1569578852 结果失真
所以只有对 32位浮点数(经测试,有效范围为:[-2^31,2^31-1]
,即[-2147483648,2147483647]
) 进行按位运算时才会得到期望的结果。
~~2147483647.1 // => 2147483647 正确
~~2147483648.1 // => -2147483648 不正确
~~-2147483648.1 // => -2147483648 正确
~~-2147483649.1 // => 2147483647 不正确
需要注意的是,如果整数部分和小数部分数字之和超过了 16 位(不包括小数点),那么双重按位非操作符的结果也会不正确:( Number编码的精度 的算术会受到舍入的影响。)
使用场景
对一些函数入参校验及处理方面有用,比如传入的可能是任意数字,需要排除掉极端情况(NaN
,Infinity
),然后取整;
function fn(){
var param = arguments[1]
if(param === 'number' && !isNaN(foo) && foo !== Infinity){
var value = Number(param) || 0;
value = (value < 0)
? Math.ceil(value)
: Math.floor(value);
}
}
使用 ~~
后:
function fn(){
var value = ~~arguments[1]
}
拓展
左移(<<)和右移(>>)运算符可以用来进行快速的二进制乘法和除法操作。左移一个数值实际上等于将这个数值乘以2的某个幂,而右移一个数值则等于将这个数值除以2的某个幂(忽略余数)。
但这个使用场景只适用对 2 的乘法除法操作(。。。)
let result = 6 * 2; // 结果是12
let base = 6; // 二进制表示为 110
let shift = 1; // 左移1位
let result = base << shift; // 结果是12,二进制表示为 1100
let result = 12 / 2; // 结果是6,但可能得到一个浮点数 let roundedResult = Math.floor(12 / 2); // 结果是6,确保得到整数
let base = 12; // 二进制表示为 1100
let shift = 1; // 右移1位
let result = base >> shift; // 结果是6,二进制表示为 110
总结
本文探讨了使用双重按位非运算符 ~~
对操作数取整的原理。
~~
之所以可以用来取整,是因为按位运算操作数转化为 32 位的有符号整型,会舍弃掉小数部分。并且按位非运算(~
)时,任何数字x
(已被转化为 32 位有符号整型) 的运算结果都是-(x + 1)
。那么双重按位非(~~
)对数字的运算结果就是-(-(x + 1) + 1)
,结果就是x
。- 操作数是数字并且在位运算的有效范围内(
[-2^31,2^31-1]
),~~
取整才会得到期望的结果。 - 使用场景方面对一些函数入参校验及处理方面可能有用
折腾完毕 🤪。