2

双重按位非运算符 ~~ 对数字取整 - xingba-coder

 7 months ago
source link: https://www.cnblogs.com/zsxblog/p/17999178
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

双重按位非运算符 ~~ 对数字取整

按位非运算符(~)将操作数的位反转。它将操作数转化为 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

详细的转换过程:

  1. 调用对象的valueOf方法
    如果该方法返回一个原始值,JavaScript会尝试将这个原始值转换为一个数字。如果valueOf方法返回的还是一个对象,JavaScript会继续调用对象的toString方法。

  2. 调用对象的toString方法
    这个方法返回一个字符串,然后JavaScript会尝试将这个字符串转换为一个数字。

  3. 转换字符串为数字
    一旦从valueOftoString方法中获得了一个原始值(通常是字符串),JavaScript会按照字符串到数字的转换规则来处理这个值。

如果valueOftoString返回的值是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

再比如 ~~ 操作日期类型数据,DatevalueOf 方法返回以数值格式表示的一个 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编码的精度 的算术会受到舍入的影响。)

image
image

对一些函数入参校验及处理方面有用,比如传入的可能是任意数字,需要排除掉极端情况(NaNInfinity),然后取整;

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]),~~ 取整才会得到期望的结果。
  • 使用场景方面对一些函数入参校验及处理方面可能有用

折腾完毕 🤪。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK