溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

前端大數(shù)的運(yùn)算及相關(guān)知識(shí)有哪些

發(fā)布時(shí)間:2021-10-20 16:48:06 來源:億速云 閱讀:180 作者:iii 欄目:web開發(fā)

本篇內(nèi)容主要講解“前端大數(shù)的運(yùn)算及相關(guān)知識(shí)有哪些”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實(shí)用性強(qiáng)。下面就讓小編來帶大家學(xué)習(xí)“前端大數(shù)的運(yùn)算及相關(guān)知識(shí)有哪些”吧!

 背景

前段時(shí)間我在公司的項(xiàng)目中負(fù)責(zé)的是權(quán)限管理這一塊的需求。需求的大概內(nèi)容就是系統(tǒng)的管理員可以在用戶管理界面對(duì)用戶和用戶扮演的角色進(jìn)行增刪改查的操作,然后當(dāng)用戶進(jìn)入主應(yīng)用時(shí),前端會(huì)請(qǐng)求到一個(gè)表示用戶權(quán)限的數(shù)組usr_permission,前端通過usr_permission來判斷用戶是否擁有某項(xiàng)權(quán)限。

這個(gè)usr_permission是一個(gè)長度為16的大數(shù)字符串?dāng)?shù)組,如下所示:

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

數(shù)組中的每一個(gè)元素可以轉(zhuǎn)成64位的二進(jìn)制數(shù),二進(jìn)制數(shù)中的每一位通過0和1表示一種權(quán)限,這樣每一個(gè)元素可以表示64種權(quán)限,整個(gè)usr_permission就可以表示16*64=1024種權(quán)限。后端之所以要對(duì)usr_permission進(jìn)行壓縮,是因?yàn)楹蠖瞬捎玫氖俏⒎?wù)架構(gòu),各個(gè)模塊在通信的過程中通過在請(qǐng)求頭中加入usr_permission來做權(quán)限的認(rèn)證。

數(shù)組usr_permission的第0個(gè)元素表示第[0, 63]號(hào)的權(quán)限,第1個(gè)元素表示第[64, 127]號(hào)的權(quán)限,以此類推。比如現(xiàn)在我們要查找第220號(hào)權(quán)限:

const permission = 220 // 查看銷售出庫 const usr_permission = [   "17310727576501632001",     "1081919648897631175",     "4607248419625398332",     "18158795172266376960",     "18428747250223005711",     "17294384420617192448",     "216384094707056832",     "13902625308286185532",     "275821367043",     "0",     "0",     "0",     "0",     "0",     "0",     "0", ]  // "18158795172266376960" 表示第193號(hào)~第256號(hào)權(quán)限 // 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號(hào)權(quán)限由第3個(gè)元素"18158795172266376960"表示。

  • 我們將"18158795172266376960"轉(zhuǎn)成二進(jìn)制得到1111 1100 0000 0000 1111 1111 1111 1111 1111 0000 0000 0011 1111 1111 0000 0000。

  • 將220除以64得到余數(shù)28,也就是說二進(jìn)制數(shù)1111 1100 0000 0000 1111 1111 1111 1111 1111 0000 0000 0011 1111 1111 0000 0000從右數(shù)的第28位表示第220號(hào)權(quán)限。

  • 我們可以將二進(jìn)制數(shù)1111 1100 0000 0000 1111 1111 1111 1111 1111 0000 0000 0011 1111 1111 0000 0000右移28位,將表示第220號(hào)權(quán)限的位數(shù)推到最低位。

  • 然后將二進(jìn)制數(shù)與1進(jìn)行按位與操作,如果當(dāng)前用戶擁有第220號(hào)權(quán)限,則最后得到的結(jié)果為1,反之為0。

以上就是前端查找權(quán)限的大致過程,那么這個(gè)代碼要怎么寫呢?在編寫代碼之前,我們先來復(fù)習(xí)一下JavaScript大數(shù)相關(guān)的知識(shí),了解編寫代碼的過程中會(huì)遇到什么問題。

IEEE 754標(biāo)準(zhǔn)

在計(jì)算機(jī)組成原理這門課里我們學(xué)過,在以IEEE 754為標(biāo)準(zhǔn)的浮點(diǎn)運(yùn)算中,有兩種浮點(diǎn)數(shù)值表示方式,一種是單精度(32位),還有一種是雙精度(64位)。

前端大數(shù)的運(yùn)算及相關(guān)知識(shí)有哪些

在IEEE 754標(biāo)準(zhǔn)中,一個(gè)數(shù)字被表示成 +1.0001x2^3 這種形式。比如說在單精度(32位)表示法中,有1位用來表示數(shù)字的正負(fù)(符號(hào)位),8位用來表示2的冪次方(指數(shù)偏移值E,需要減去一個(gè)固定的數(shù)字得到指數(shù)e),23位表示1后面的小數(shù)位(尾數(shù))。

比如0 1000 0010 0001 0000 0000 0000 0000 000,第1位0表示它是正數(shù),第[2, 9]位1000 0010轉(zhuǎn)換成十進(jìn)制就是130,我們需要減去一個(gè)常數(shù)127得到3,也就是這個(gè)數(shù)字需要乘以2的三次方,第[10, 32]位則表示1.0001 0000 0000 0000 0000 000,那么這個(gè)數(shù)字表示的就是二級(jí)制中的 +1.0001*2^3 ,轉(zhuǎn)換成十進(jìn)制也就是8.5。

前端大數(shù)的運(yùn)算及相關(guān)知識(shí)有哪些

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

JavaScript 就是采用IEEE754 標(biāo)準(zhǔn)定義的64 位浮點(diǎn)格式表示數(shù)字。在64位浮點(diǎn)格式中,有52位可以表示小數(shù)點(diǎn)后面的數(shù)字,加上小數(shù)點(diǎn)前面的1,就有53位可以用來表示數(shù)字,也就是說64位浮點(diǎn)可以表示的最大的數(shù)字是 2^53-1 ,超過 2^53-1 的數(shù)字就會(huì)發(fā)生精度丟失。因?yàn)?^53用64位浮點(diǎn)格式表示就變成了這樣:

符號(hào)位:0 指數(shù):53 尾數(shù):1.000000...000 (小數(shù)點(diǎn)后一共52個(gè)0)

小數(shù)點(diǎn)后面的第53個(gè)0已經(jīng)被丟棄了,那么 2^53+1 的64位浮點(diǎn)格式就會(huì)變得和 2^53 一樣。一個(gè)浮點(diǎn)格式可以表示多個(gè)數(shù)字,說明這個(gè)數(shù)字是不安全的。所以在JavaScript中,最大的安全數(shù)是 2^53-1 ,這樣就保證了一個(gè)浮點(diǎn)格式對(duì)應(yīng)一個(gè)數(shù)字。

0.1 + 0.2 !== 0.3

有一道很常見的前端面試題,就是問你為什么JavaScript中0.1+0.2為什么不等于0.3?0.1轉(zhuǎn)換成二進(jìn)制是0.0 0011 0011 0011 0011 0011 0011 ... (0011循環(huán)),0.2轉(zhuǎn)換成二進(jìn)制是0.0011 0011 0011 0011 0011 0011 0011 ... (0011循環(huán)),用64位浮點(diǎn)格式表示如下:

// 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指數(shù)不一致,需要進(jìn)行對(duì)階操作 // 對(duì)階操作,會(huì)產(chǎn)生精度丟失 // 之所以選0.1進(jìn)行對(duì)階操作是因?yàn)橛乙茙淼木葋G失遠(yuǎn)遠(yuǎn)小于左移帶來的溢出 e = -3; m = 0.1100110011001100110011001100110011001100110011001101 (52位) + e = -3; m = 1.1001100110011001100110011001100110011001100110011010 (52位)   e = -3; m = 10.0110011001100110011001100110011001100110011001100111 (52位)  // 發(fā)生精度丟失 e = -2; m = 1.00110011001100110011001100110011001100110011001100111 (53位)

我們看到已經(jīng)溢出來了(超過了52位),那么這個(gè)時(shí)候我們就要做四舍五入了,那怎么舍入才能與原來的數(shù)最接近呢?比如1.101要保留2位小數(shù),那么結(jié)果有可能是 1.10 和 1.11 ,這個(gè)時(shí)候兩個(gè)都是一樣近,我們?nèi)∧囊粋€(gè)呢?規(guī)則是保留偶數(shù)的那一個(gè),在這里就是保留 1.10。

回到我們之前的就是取m=1.0011001100110011001100110011001100110011001100110100 (52位)

然后我們得到最終的二進(jìn)制數(shù):

1.0011001100110011001100110011001100110011001100110100 * 2 ^ -2

=0.010011001100110011001100110011001100110011001100110100

轉(zhuǎn)換成十進(jìn)制就是0.30000000000000004,所以,所以0.1 + 0.2 的最終結(jié)果是0.30000000000000004。

BigInt

通過前面的講解,我們清晰地認(rèn)識(shí)到在以前,JavaScript是沒有辦法對(duì)大于 2^53-1 的數(shù)字進(jìn)行處理的。不過后來,JavaScript提供了內(nèi)置對(duì)象BigInt來處理大數(shù)。 BigInt 可以表示任意大的整數(shù)??梢杂迷谝粋€(gè)整數(shù)字面量后面加 n 的方式定義一個(gè) BigInt ,如: 10n ,或者調(diào)用函數(shù) 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實(shí)現(xiàn)的權(quán)限查找代碼如下:

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存在兼容性問題:

前端大數(shù)的運(yùn)算及相關(guān)知識(shí)有哪些

根據(jù)我司用戶使用瀏覽器版本數(shù)據(jù)的分析,得到如下餅狀圖:

前端大數(shù)的運(yùn)算及相關(guān)知識(shí)有哪些

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

解決兼容性的問題,一種方式是如果希望在項(xiàng)目中繼續(xù)使用BigInt,那么需要Babel的一些插件進(jìn)行轉(zhuǎn)換。這些插件需要調(diào)用一些方法去檢測運(yùn)算符什么時(shí)候被用于BigInt,這將導(dǎo)致不可接受的性能損失,而且在很多情況下是行不通的。另外一種方法就是找一些封裝大數(shù)運(yùn)算方法的第三方庫,使用它們的語法做大數(shù)運(yùn)算。

用第三方庫實(shí)現(xiàn)

很多第三方庫可以用來做大數(shù)運(yùn)算,大體的思路就是定義一個(gè)數(shù)據(jù)結(jié)構(gòu)來存放大數(shù)的正負(fù)及數(shù)值,分別算出每一位的結(jié)果再存儲(chǔ)到數(shù)據(jù)結(jié)構(gòu)中。

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) {     // 開發(fā)環(huán)境不受權(quán)限限制     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   }

權(quán)限查找新思路

后來,一位同事提到了一種新的權(quán)限查找的解決方案:前端獲取到數(shù)組usr_permission以后,將usr_permission的所有元素轉(zhuǎn)成二進(jìn)制,并進(jìn)行字符串拼接,得到一個(gè)表示用戶所有權(quán)限的字符串permissions。當(dāng)需要查找權(quán)限時(shí),查找permissions對(duì)應(yīng)的位數(shù)即可。這樣相當(dāng)于在用戶進(jìn)入系統(tǒng)時(shí)就將所有的權(quán)限都算好,而不是用一次算一次。

在中學(xué)時(shí),我們學(xué)到的將十進(jìn)制轉(zhuǎn)成二進(jìn)制的方法是輾轉(zhuǎn)相除法,這里有一種新思路:

  • 比如我們要用5個(gè)二進(jìn)制位表示11這個(gè)數(shù)

  • 我們需要先定義一個(gè)長度為5,由2的倍數(shù)組成的數(shù)組[16, 8, 4, 2, 1],然后將11與數(shù)組中的元素挨個(gè)比較

  • 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,結(jié)束

  • 所以用5位二進(jìn)制數(shù)表示11的結(jié)果就是01011

根據(jù)上面的思路可以得到的代碼如下,這里用big.js這個(gè)包去實(shí)現(xiàn):

import Big from 'big.js'         import _ from 'lodash'      permissions = '' // 最后生成的權(quán)限字符串      // 生成長度為64,由2的倍數(shù)組成的數(shù)組     generateBinaryArray(bits: number) {       const arr: any[] = []       _.each(_.range(bits), (index) => {         arr.unshift(Big(2).pow(index))       })       return arr     }        // 將usr_permission中單個(gè)元素轉(zhuǎn)成二進(jìn)制     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中所有元素的二進(jìn)制形式進(jìn)行拼接   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   }      // 判斷時(shí)候擁有某項(xiàng)權(quán)限   hasPermission(permission: Permission) {     if (!this.permissions) {       return false     }     return this.permissions[permission] === '1'   }

到此,相信大家對(duì)“前端大數(shù)的運(yùn)算及相關(guān)知識(shí)有哪些”有了更深的了解,不妨來實(shí)際操作一番吧!這里是億速云網(wǎng)站,更多相關(guān)內(nèi)容可以進(jìn)入相關(guān)頻道進(jìn)行查詢,關(guān)注我們,繼續(xù)學(xué)習(xí)!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請(qǐng)聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI