談談JS中的~

前言

最近在閱讀Koa2源碼,在閱讀過程當中get到了一些特別的技巧:就是關於~這個傢伙的。爲此作了一些調研,把學到的東西分享給你們~~javascript

~ 是啥

非科班出身的同窗可能會對這個~符號比較懵,這裏簡單介紹一下(科班同窗就能夠跳過啦)。~是位運算符的一種,它的做用是按位取反。java

咱們都知道,計算機其實只認兩個符號:0、1,但其實在計算機中,數值一概是用補碼來表示和存儲,所以位運算也是基於補碼運算的。git

這裏又涉及到了補碼的概念,因此簡單介紹一下原碼、補碼:github

  1. 正數的原碼、補碼是它自己的二進制
  2. 負數的原碼是它的二進制,從左往右數第一位做爲符號位,1表明負數。如:-1的原碼是 1 000 0001
  3. 負數的補碼是它的原碼的非符號位的按位取反加1。舉個🌰:-1的原碼是1000 0001 (左邊第一個1就是符號位,1表明負數),除非符號位按位取反加1後是 1 111 1111。

~ 是如何實現的

JS中的~與其餘語言中的~略有不一樣,這裏先簡單介紹一下比較常規的~。數組

在對正數進行~操做時:函數

  1. 將正數的原碼轉換爲補碼(都是它的二進制啦)
  2. 對補碼按位取反 (注意二進制第一個符號位,本來是0,表明正數)
  3. 正數的補碼按位取反後,計算機:你這小子第一位如今是1啊,是個負數的補碼
  4. 計算機將補碼轉換爲原碼(轉換規則也是非符號位的按位取反加1)
  5. 將原碼轉換對應進制輸出(10進制)

舉個實例:性能

  1. 3的原碼補碼都是 0000 0011
  2. 按位取反後 1111 1100
  3. 補碼轉換爲原碼 1 000 0100
  4. 輸出10進制:-4

對負數進行~操做時:測試

  1. 將負數的原碼轉換爲補碼(強調下:負數的補碼是它的原碼的非符號位的按位取反加1)
  2. 補碼按位取反
  3. 計算機:你小子原來是個正數啊,原碼、補碼都同樣了,真好辦
  4. 補碼直接輸出對應進制

舉個實例:ui

  1. -3的原碼是 1000 0011
  2. -3的補碼是 1111 1101
  3. 按位取反後 0000 0010
  4. 輸出10進制:2

總結:~x = -x - 1,如 ~3 = -3-1 = -4 ;~(-3) = 3 - 1 = 2es5

而在JS裏,~會首先對運算對象進行轉換爲整數的操做,轉換規則參考ECMA-262規範的ToInt32。這裏簡單翻譯一下,假設咱們輸入的參數爲input;

  1. let number = ToNumber(input) (ToNumber函數定義)
    1. 關於ToNumber簡單提一下,若是參數類型是Undefined 返回 NaN
    2. 若是是Null,返回 +0
    3. 若是是布爾值,值爲true,返回1,值爲false,返回0
    4. 若是是數字,返回它自己
    5. 若是是字符串:參照規則9.3.1,規則比較多,不一一敘述了
    6. 若是是對象:
      1. let primValue = ToPrimitive(input argument, hint Number) (ToPrimitive定義;第一個參數爲輸入的變量,第二個爲控制變量,若是輸入的變量能夠轉換爲多個基本類型,能夠用第二個變量控制)
      2. 返回 ToNumber(primValue)
  2. 若是number值是NaN, +0, −0, +∞, 或者 −∞,則返回 +0
  3. let posInt = sign(number) * floor(abs(number))
    1. sign() 函數根據輸入內容是正數仍是負數,返回1或者-1,可是沒法用於0;(sign定義)
    2. abs 取number的絕對值 (abs定義)
    3. floor 取最接近該number且不大於它自己的整數 (floor 定義)
  4. let int32bit = posInt modulo 232
    1. modulo 爲取模運算 (modulo定義)
  5. 若是 int32bit >= 231,返回 int32bit - 232,不然返回 int32bit

舉個例子:~undefined,在轉換過程當中,ToNumber(undefined)結果爲NaN,ToInt32(NaN)結果爲+0,所以~undefined結果與~+0一致,爲-1。另外對於對象的轉換,核心規則主要根據[[DefaultValue]] (hint)。下面各舉一些對象轉換的例子:

> ~{}
-1
> ~[]
-1
> ~NaN
-1
> ~[1]
-2
> ~[2,3]
-1
> ~{toString: () => '45'}
-46
> ~{toString: () => '45',valueOf: () => 123}
-124
複製代碼

~ 妙用

  1. 對於-1,在JS中有個很出名的函數是indexOf(),若是indexOf返回-1 即表示要找的內容在目標數組、字符串中不存在。
const target = [1,4,56,7]
console.log(!!~target.indexOf(8)) //false
// 經過~轉換爲0後,不少處理能夠簡化,
// 能夠免去寫 !== -1 或者 === -1等煩惱
複製代碼
  1. 注意下 ~x = -x -1,那麼 ~~x = ~(~x) = - (-x - 1) - 1 = x, 即 ~~x == x自己。剛纔也提到,在進行~x時,會先通過整數處理,所以~還有轉換爲整數的巧妙做用
const target = '321'
console.log(typeof ~~target) // number
複製代碼
  1. 剛纔也提到JS是進行轉換整數的處理,對於正浮點數,採起的是相似Math.floor的處理,便可以用~~去代替Math.floor,而負浮點數是相似Math.ceil的處理。總而言之只取整數部分。
const target1 = '321.235'
console.log(~~target1) // 321

const target2 = '-321.77'
console.log(~~target2) // -321
複製代碼
  1. ~ 與 ! ~與!雖然邏輯不一樣,可是在某些場合下能起到相同的做用
> ~~undefined == !!undefined
true
> ~~null == !!null
true
> ~~0 == !!0
true
> ~~1 == !!1
true
// 注意,並非全部同樣的輸入,~、!計算結果都相等
> ~~'a' == !!'a'
false
> ~~[0] == !![0]
false
> ~~-1 == !!-1
false
> ~~2 == !!2
false
複製代碼

~ 速度比較

上面提到了~的幾種用處,歸根結底都是利用了~能快速轉換整數的能力,下面分別對~~與其它經常使用的轉換操做進行簡單的速度比較,測試環境:macOS Mojave 10.14.2 Node v8.12.0

let {numArr,stringArr} = require('./data');
let func1 = number => {
  let start = performance.now();
  let b = ~~number; // 測試符號:包括 ~ 、Math.floor、parseInt、+ 四種
  return {
    value: b,
    time: performance.now() - start
  };
};
let totalTime = 0;
numArr.map(item => {
  totalTime += func1(item).time
})
console.log({
  type: 'fun1',
  totalTime,
  len: numArr.length,
  average: totalTime / numArr.length
})


複製代碼

因爲篇幅問題,這裏不放全全部代碼了,主要修改內容爲有測試的運算符號以及數據來源。數據爲MockJS生成隨機浮點數數組,包含正負浮點數、正負浮點字符串、隨機Null、undefined、NaN數組;

  1. 測試轉換內容爲±浮點數:
操做符號 測試數量 總耗時 平均時間
~~ 609 0.24905507266521454 0.00040895742637966264
Math.floor 609 0.34583795070648193 0.00056787840838502780
parseInt 609 0.28167189657688140 0.00046251542951868867
+ 609 0.31073787808418274 0.00051024282115629350
  1. 測試轉換內容爲±字符串浮點數:
操做符號 測試數量 總耗時 平均時間
~~ 701 0.4971509501338005 0.0007092024966245371
Math.floor 701 0.5587870776653290 0.0007971284988093138
parseInt 701 0.5465480908751488 0.0007796691738589855
+ 701 0.4985470473766327 0.00071119407614355590
  1. 測試內容爲undefined、null、NaN:
操做符號 測試數量 總耗時 平均時間
~~ 2187 2.5649426132440567 0.0011728132662295642
!! 2187 2.2864151149988170 0.0010454572999537344

在JS中,因爲有參數的轉換,~~必然會存在性能的損耗,可是這個損耗在咱們能夠接受的範圍內,對比上面幾組測試結果可得知,同是數字的狀況下,~速度最快,而其餘兩種狀況因爲轉換的緣由,略有下降。

題外話

在測試過程當中,還發現了一個有意思的事,發現用console、performance測量時,第一個耗時較長,有圖有真相:

第一、2張圖是console.time、performance.now計算的結果,在第二張圖時還互換了下兩個函數調用順序。猜想是第一次調用console.time、performance須要進行某些初始化。

相關文章
相關標籤/搜索