雙重按位非運算子 ~~ 對數字取整

xingba-coder發表於2024-01-31

介紹

按位非運算子(~)將運算元的位反轉。它將運算元轉化為 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]),~~ 取整才會得到期望的結果。
  • 使用場景方面對一些函式入參校驗及處理方面可能有用

折騰完畢 🤪。

相關文章