前端大数的运算及相关知识总结

背景

前段时间我在公司的项目中负责的是权限管理这一块的需求。需求的大概内容就是系统的管理员能够在用户管理界面对用户和用户扮演的角色进行增删改查的操做,而后当用户进入主应用时,前端会请求到一个表示用户权限的数组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'
  }
相关文章
相关标签/搜索