前端大數的運算及相關知識總結

背景

前段時間我在公司的項目中負責的是權限管理這一塊的需求。需求的大概內容就是系統的管理員能夠在用戶管理界面對用戶和用戶扮演的角色進行增刪改查的操做,而後當用戶進入主應用時,前端會請求到一個表示用戶權限的數組usr_permission,前端經過usr_permission來判斷用戶是否擁有某項權限。前端

這個usr_permission是一個長度爲16的大數字符串數組,以下所示:面試

const usr_permission = [
  "17310727576501632001",
    "1081919648897631175",
    "4607248419625398332",
    "18158795172266376960",
    "18428747250223005711",
    "17294384420617192448",
    "216384094707056832",
    "13902625308286185532",
    "275821367043",
    "0",
    "0",
    "0",
    "0",
    "0",
    "0",
    "0",
]

數組中的每個元素能夠轉成64位的二進制數,二進制數中的每一位經過0和1表示一種權限,這樣每個元素能夠表示64種權限,整個usr_permission就能夠表示16*64=1024種權限。後端之因此要對usr_permission進行壓縮,是由於後端採用的是微服務架構,各個模塊在通訊的過程當中經過在請求頭中加入usr_permission來作權限的認證。後端

數組usr_permission的第0個元素表示第[0, 63]號的權限,第1個元素表示第[64, 127]號的權限,以此類推。好比如今咱們要查找第220號權限:數組

const permission = 220 // 查看銷售出庫
const usr_permission = [
  "17310727576501632001",
    "1081919648897631175",
    "4607248419625398332",
    "18158795172266376960",
    "18428747250223005711",
    "17294384420617192448",
    "216384094707056832",
    "13902625308286185532",
    "275821367043",
    "0",
    "0",
    "0",
    "0",
    "0",
    "0",
    "0",
]

// "18158795172266376960" 表示第193號~第256號權限
// 1111 1100 0000 0000 1111 1111 1111 1111 1111 0000 0000 0011 1111 1111 0000 0000
// 220 % 64 = 28
// 0000 0000 0000 0000 0000 0000 0000 1111 1100 0000 0000 1111 1111 1111 1111 1111
// 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0001
// -------------------------------------------------------------------------------
// 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0001
  • 從usr_permission中咱們得知第220號權限由第3個元素"18158795172266376960"表示。
  • 咱們將"18158795172266376960"轉成二進制獲得1111 1100 0000 0000 1111 1111 1111 1111 1111 0000 0000 0011 1111 1111 0000 0000。
  • 將220除以64獲得餘數28,也就是說二進制數1111 1100 0000 0000 1111 1111 1111 1111 1111 0000 0000 0011 1111 1111 0000 0000從右數的第28位表示第220號權限。
  • 咱們能夠將二進制數1111 1100 0000 0000 1111 1111 1111 1111 1111 0000 0000 0011 1111 1111 0000 0000右移28位,將表示第220號權限的位數推到最低位。
  • 而後將二進制數與1進行按位與操做,若是當前用戶擁有第220號權限,則最後獲得的結果爲1,反之爲0。

以上就是前端查找權限的大體過程,那麼這個代碼要怎麼寫呢?在編寫代碼以前,咱們先來複習一下JavaScript大數相關的知識,瞭解編寫代碼的過程當中會遇到什麼問題。瀏覽器

IEEE 754標準

在計算機組成原理這門課裏咱們學過,在以IEEE 754爲標準的浮點運算中,有兩種浮點數值表示方式,一種是單精度(32位),還有一種是雙精度(64位)。安全

1*JqRzcCeJp3FnbixVwSi1UQ.png

在IEEE 754標準中,一個數字被表示成 +1.0001x2^3 這種形式。好比說在單精度(32位)表示法中,有1位用來表示數字的正負(符號位),8位用來表示2的冪次方(指數偏移值E,須要減去一個固定的數字獲得指數e),23位表示1後面的小數位(尾數)。數據結構

好比0 1000 0010 0001 0000 0000 0000 0000 000,第1位0表示它是正數,第[2, 9]位1000 0010轉換成十進制就是130,咱們須要減去一個常數127獲得3,也就是這個數字須要乘以2的三次方,第[10, 32]位則表示1.0001 0000 0000 0000 0000 000,那麼這個數字表示的就是二級制中的+1.0001*2^3,轉換成十進制也就是8.5。架構

1*tu8UHXww5mM6ndUVNA_dAg.png

同理,雙精度(64位)也是同樣的表現形式,只是在64位中有11位用來表示2的冪次方,52位用來表示小數位。函數

JavaScript 就是採用IEEE754 標準定義的64 位浮點格式表示數字。在64位浮點格式中,有52位能夠表示小數點後面的數字,加上小數點前面的1,就有53位能夠用來表示數字,也就是說64位浮點能夠表示的最大的數字是2^53-1,超過2^53-1的數字就會發生精度丟失。由於2^53用64位浮點格式表示就變成了這樣:微服務

符號位:0 指數:53 尾數:1.000000...000 (小數點後一共52個0)

小數點後面的第53個0已經被丟棄了,那麼2^53+1的64位浮點格式就會變得和2^53同樣。一個浮點格式能夠表示多個數字,說明這個數字是不安全的。因此在JavaScript中,最大的安全數是2^53-1,這樣就保證了一個浮點格式對應一個數字。

0.1 + 0.2 !== 0.3

有一道很常見的前端面試題,就是問你爲何JavaScript中0.1+0.2爲何不等於0.3?0.1轉換成二進制是0.0 0011 0011 0011 0011 0011 0011 ... (0011循環),0.2轉換成二進制是0.0011 0011 0011 0011 0011 0011 0011 ... (0011循環),用64位浮點格式表示以下:

// 0.1
e = -4;
m = 1.1001100110011001100110011001100110011001100110011010 (52位)

// 0.2
e = -3;
m = 1.1001100110011001100110011001100110011001100110011010 (52位)

而後把它們相加:

e = -4; m = 1.1001100110011001100110011001100110011001100110011010 (52位)
+
e = -3; m = 1.1001100110011001100110011001100110011001100110011010 (52位)

// 0.1和0.2指數不一致,須要進行對階操做
// 對階操做,會產生精度丟失
// 之因此選0.1進行對階操做是由於右移帶來的精度丟失遠遠小於左移帶來的溢出
e = -3; m = 0.1100110011001100110011001100110011001100110011001101 (52位)
+
e = -3; m = 1.1001100110011001100110011001100110011001100110011010 (52位)


e = -3; m = 10.0110011001100110011001100110011001100110011001100111 (52位)

// 發生精度丟失
e = -2; m = 1.00110011001100110011001100110011001100110011001100111 (53位)

咱們看到已經溢出來了(超過了52位),那麼這個時候咱們就要作四捨五入了,那怎麼舍入才能與原來的數最接近呢?好比1.101要保留2位小數,那麼結果有多是 1.10 和 1.11 ,這個時候兩個都是同樣近,咱們取哪個呢?規則是保留偶數的那一個,在這裏就是保留 1.10。

回到咱們以前的就是取m=1.0011001100110011001100110011001100110011001100110100 (52位)

而後咱們獲得最終的二進制數:

1.0011001100110011001100110011001100110011001100110100 * 2 ^ -2

=0.010011001100110011001100110011001100110011001100110100

轉換成十進制就是0.30000000000000004,因此,因此0.1 + 0.2 的最終結果是0.30000000000000004。

BigInt

經過前面的講解,咱們清晰地認識到在之前,JavaScript是沒有辦法對大於2^53-1的數字進行處理的。不事後來,JavaScript提供了內置對象BigInt來處理大數。BigInt 能夠表示任意大的整數。能夠用在一個整數字面量後面加 n 的方式定義一個 BigInt ,如:10n,或者調用函數BigInt()

const theBiggestInt = 9007199254740991n;

const alsoHuge = BigInt(9007199254740991);
// ↪ 9007199254740991n

const hugeString = BigInt("9007199254740991");
// ↪ 9007199254740991n

typeof 1n === 'bigint'; // true
typeof BigInt('1') === 'bigint'; // true

0n === 0 // ↪ false

0n == 0 // ↪ true

用BigInt實現的權限查找代碼以下:

hasPermission(permission: Permission) {
    const usr_permissions = this.userInfo.usr_permissions
    const arr_index = Math.floor(permission / 64)
    const bit_index = permission % 64
    if (usr_permissions && usr_permissions.length > arr_index) {
      if ((BigInt(usr_permissions[arr_index]) >> BigInt(bit_index)) & 1n) {
        return true
      }
    }
    return false
}

兼容分析

可是BigInt存在兼容性問題:

image-20201209043607744.png

根據我司用戶使用瀏覽器版本數據的分析,獲得以下餅狀圖:

image-20201209045125837.png

不兼容BigInt瀏覽器的比例佔到12.4%

解決兼容性的問題,一種方式是若是但願在項目中繼續使用BigInt,那麼須要Babel的一些插件進行轉換。這些插件須要調用一些方法去檢測運算符何時被用於BigInt,這將致使不可接受的性能損失,並且在不少狀況下是行不通的。另一種方法就是找一些封裝大數運算方法的第三方庫,使用它們的語法作大數運算。

用第三方庫實現

不少第三方庫能夠用來作大數運算,大致的思路就是定義一個數據結構來存放大數的正負及數值,分別算出每一位的結果再存儲到數據結構中。

jsbn 解決方案

// yarn add jsbn @types/jsbn

import { BigInteger } from 'jsbn'

hasPermission(permission: Permission) {
    const usr_permissions = this.userInfo.usr_permissions
    const arr_index = Math.floor(permission / 64)
    const bit_index = permission % 64
    if (usr_permissions && usr_permissions.length > arr_index) {
      if (
        new BigInteger(usr_permissions[arr_index])
          .shiftRight(bit_index)
          .and(new BigInteger('1'))
          .toString() !== '0'
      ) {
        return true
      }
    }
    return false
  }

jsbi 解決方案

// yarn add jsbi

import JSBI from 'jsbi'

hasPermission(permission: Permission) {
    // 開發環境不授權限限制
    if (__DEVELOPMENT__) {
      return true
    }

    const usr_permissions = this.userInfo.usr_permissions
    const arr_index = Math.floor(permission / 64)
    const bit_index = permission % 64
    if (usr_permissions && usr_permissions.length > arr_index) {
      const a = JSBI.BigInt(usr_permissions[arr_index])
      const b = JSBI.BigInt(bit_index)
      const c = JSBI.signedRightShift(a, b)
      const d = JSBI.BigInt(1)
      const e = JSBI.bitwiseAnd(c, d)
      if (e.toString() !== '0') {
        return true
      }
    }
    return false
  }

權限查找新思路

後來,一位同事提到了一種新的權限查找的解決方案:前端獲取到數組usr_permission之後,將usr_permission的全部元素轉成二進制,並進行字符串拼接,獲得一個表示用戶全部權限的字符串permissions。當須要查找權限時,查找permissions對應的位數便可。這樣至關於在用戶進入系統時就將全部的權限都算好,而不是用一次算一次。

在中學時,咱們學到的將十進制轉成二進制的方法是展轉相除法,這裏有一種新思路:

  • 好比咱們要用5個二進制位表示11這個數
  • 咱們須要先定義一個長度爲5,由2的倍數組成的數組[16, 8, 4, 2, 1],而後將11與數組中的元素挨個比較
  • 11 < 16, 因此獲得[0, x, x, x, x]
  • 11 >= 8,因此獲得[0, 1, x, x, x],11 - 8 = 3
  • 3 < 4,因此獲得[0, 1, 0, x, x]
  • 3 >= 2,因此獲得[0, 1, 0, 1, x],3 - 2 = 1
  • 1>= 1,因此獲得[0, 1, 0, 1, 1],1 - 1 = 0,結束
  • 因此用5位二進制數表示11的結果就是01011

根據上面的思路能夠獲得的代碼以下,這裏用big.js這個包去實現:

import Big from 'big.js'    
    import _ from 'lodash'

    permissions = '' // 最後生成的權限字符串

    // 生成長度爲64,由2的倍數組成的數組
    generateBinaryArray(bits: number) {
      const arr: any[] = []
      _.each(_.range(bits), (index) => {
        arr.unshift(Big(2).pow(index))
      })
      return arr
    }  

    // 將usr_permission中單個元素轉成二進制
    translatePermission(binaryArray: any[], permission: string) {
    let bigPermission = Big(permission)
    const permissionBinaryArray: number[] = []
    _.each(binaryArray, (v, i) => {
      if (bigPermission.gte(binaryArray[i])) {
        bigPermission = bigPermission.minus(binaryArray[i])
        permissionBinaryArray.unshift(1)
      } else {
        permissionBinaryArray.unshift(0)
      }
    })
    return permissionBinaryArray.join('')
  }

    // 將usr_permission中全部元素的二進制形式進行拼接
  generatePermissionString() {
    const usr_permissions = this.userInfo.usr_permissions
    let str = ''
    const binaryArray = this.generateBinaryArray(64)
    _.each(usr_permissions, (permission, index) => {
      str = `${str}${this.translatePermission(binaryArray, permission)}`
    })
    this.permissions = str
  }

    // 判斷時候擁有某項權限
  hasPermission(permission: Permission) {
    if (!this.permissions) {
      return false
    }
    return this.permissions[permission] === '1'
  }
相關文章
相關標籤/搜索