背景前段时间在公司项目中负责权限管理的需求。需求的大致内容是系统管理员可以在用户管理界面上对用户及用户扮演的角色进行增删改查,然后当用户进入主应用时,前端会请求一个arrayusr_permission代表用户权限,前端会通过usr_permission来判断用户是否有某个权限。这个usr_permission是一个长度为16的大字符串数组,如下所示:constusr_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号权限:constpermission=220//查看销售出库constusr_permission=["17310727576501632001","1081919648897631175","4607248419625398332","18158795172266376960","18428747250223005711","17294384420617192448","216384094707056832","13902625308286185532","275821367043","0","0","0","0","0","0","0",]//"18158795172266376960"meansNo.193~No.256权限//1111110000000000111111111111111111110000000000111111111100000000//220%64=28//0000000000000000000000000000111111000000000011111111111111111111//0000000000000000000000000000000000000000000000000000000000000001//-------------------------------------------------------------------------------//0000000000000000000000000000000000000000000000000000001Fromusr_permission,weknowthatthe220thpermissionisrepresentedbythethirdelement"18158795172266376960".我们将“18158795172266376960”转换为二进制,以获取11111100000000000000111111111111111111111111111111111111000000000000001111111111111111111111111111111111111111111111111111111111111111111111换220111111110000000000111111111100000000右起第28位代表第220个权限。我们可以将二进制数1111110000000000111111111111111111110000000000111111111100000000右移28位,将代表第220位权限的位推到最低位。然后对二进制数和1进行按位与运算,如果当前用户有220号权限,则最终结果为1,否则为0。以上就是前端查找权限的大致流程,那么如何写这段代码?在写代码之前,我们先来回顾一下JavaScript大数相关的知识,了解一下在写代码的过程中会遇到哪些问题。IEEE754标准在ComputerCompositionPrinciples课程中学习过。在基于IEEE754的浮点运算中,有两种浮点数值表示,一种是单精度(32位),一种是双精度(64位)。在IEEE754标准中,数字以+1.0001x2^3的形式表示。例如,在单精度(32位)表示中,1位用于表示数的正负(符号位),8位用于表示2的次方(指数偏移值E,即需要减去一个固定的数字得到指数e),23位表示1后的小数位(尾数)。例如01000001000010000000000000000000,第一个数字0表示是正数数,而[2,9]位10000010在十进制中是130,我们需要减去一个常数127得到3,即这个数需要乘以2的立方,[10,32]位表示1.00010000000000000000000,那么这个数在二进制里就是+1.0001*2^3,换算成十进制就是8.5。同理,双精度(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.0001100110011001100110011...(0011周期),0.2转二进制为0.0011001100110011001100110011...(0011周期),用64位浮点数表示为如下://0.1e=-4;m=1.1001100110011001100110011001110011001100110011001/0.5e(0.5bits)3;M=1.10011001111001111111001100110011001100111111111111111111111111111111111111111里士-3;m=1.1001100110011001100110011001100110011001100110011010(52位)//0.1和0.2指标不一致,需要进行对订单的操作//对订单的操作会导致精度丢失//选择0.1的原因是对订单的操作丢失由右移引起的精度远小于左移52位)+e=-3;m=1.10011001100110011001100110011001100110011001100110011001110101110(52位)-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。BigIntThroughthepreviousexplanation,weclearlyrealizedthatinthepast,JavaScriptcouldnothandlenumberslargerthan2^53-1.Butlater,JavaScriptprovidedthebuilt-inobjectBigInttohandlelargenumbers.BigIntcanrepresentarbitrarilylargeintegers.YoucandefineaBigIntbyaddingnafteranintegerliteral,suchas:10n,orcallthefunctionBigInt().consttheBiggestInt=9007199254740991n;constalsoHuge=BigInt(9007199254740991);//?9007199254740991nconsthugeString=BigInt("9007199254740991");//?9007199254740991ntypeof1n==='bigint';//truetypeofBigInt('1')==='bigint';//true0n===0//?false0n==0//?true用BigInt实现的权限查询代码如下:hasPermission(permission:Permission){constusr_permissions=this.userInfo.usr_permissionsconstarr_index=Math.floor(permission/64)constbit_index=permission%64if(usr_permissions&&usr_permissions.length>arr_index){if((BigInt(usr_permissions[arr_index])>>BigInt(bit_index))&1n){returntrue}}returnfalse}兼容性分析但是BigInt存在兼容性问题:根据我们的users使用浏览器版本数据分析,得到如下饼图:不兼容BigInt的浏览器占比12.4%要解决兼容问题,一种方法是在项目中继续使用BigInt,那么你需要一些Babel插件来转换。这些插件需要调用一些方法来检测运算符何时与BigInt一起使用,这会导致不可接受的性能损失,并且在许多情况下不起作用。还有一种方法就是找一些封装大数计算方法的第三方库,用它们的语法来进行大数计算。使用第三方库实现很多第三方库可以用来做大数计算。大致的思路是定义一个数据结构来存储大数的正负值,计算出每一位的结果并存储在数据结构中。jsbn解决方案//yarnaddjsbn@types/jsbnimport{BigInteger}from'jsbn'hasPermission(permission:Permission){constusr_permissions=this.userInfo.usr_permissionsconstarr_index=Math.floor(permission/64)constbit_index=permissionspersions%64if(usr_ng&>permissions_arr_index){if(newBigInteger(usr_permissions[arr_index]).shiftRight(bit_index).and(newBigInteger('1')).toString()!=='0'){returntrue}}returnfalse}jsbi解决方案//yarnaddjsbiimportJSBIfrom'jsbi'hasPermission(permission:Permission){//开发环境不受权限限制if(__DEVELOPMENT__){returntrue}constusr_permissions=this.userInfo.usr_permissionsconstarr_index=Math.floor(permission/64)constbit_index=permission%64if(usr_permissions&&usr_ngpermissions>arr_index){consta=JSBI.BigInt(usr_permissions[arr_index])constb=JSBI.BigInt(bit_index)constc=JSBI.signedRightShift(a,b)constd=JSBI.BigInt(1)conste=JSBI.bitwiseAnd(c,d)if(e.toString()!=='0'){returntrue}}returnfalse}权限搜索新思路后来同事提到了一个新的权限查找方案:前端获取一个数组usr_permission之后,将usr_permission的所有元素转换成二进制,进行字符串拼接,得到一个代表用户所有权限的字符串permission。当需要查找权限时,只需查找权限对应的数字即可。这相当于在用户进入系统时统计所有的权限,而不是只统计一次。我们在中学的时候学过的十进制转二进制的方法就是滚除法。这里有一个新的思路:比如我们要用5个二进制位来表示数字11,我们需要先定义一个长度为5的整数倍数组成的数组[16,8,4,2,1],以及然后将11和数组中的元素一一比较11<16,所以得到[0,x,x,x,x]11>=8,所以得到[0,1,x,x,x],11-8=33<4,所以得到[0,1,0,x,x]3>=2,所以得到[0,1,0,1,x],3-2=11>=1,所以get[0,1,0,1,1],1-1=0,end,所以按照上面的思路用5个二进制数表示11的结果是01011可以得到的代码如下,其中这里使用big.js包实现:importBigfrom'big.js'import_from'lodash'permissions=''//最终生成的权限字符串//生成的长度为64,由2的倍数数组组成generateBinaryArray(bits:number){constarr:any[]=[]_.each(_.range(bits),(index)=>{arr.unshift(Big(2).pow(index))})returnarr}//转换ausr_permission中的单个元素转换为二进制translatePermission(binaryArray:any[],permission:string){letbigPermission=Big(permission)constpermissionBinaryArray:number[]=[]_.each(binaryArray,(v,i)=>{if(bigPermission.gte(binaryArray[i])){bigPermission=bigPermission。负(binaryArray[i])permissionBinaryArray.unshift(1)}else{permissionBinaryArray.unshift(0)}})returnpermissionBinaryArray.join('')}//将usr_permission中所有元素的二进制形式加入generatePermissionString(){constusr_permissions=this.userInfo.usr_permissionsletstr=''constbinaryArray=this.generateBinaryArray(64)_.each(usr_permissions,(permission,index)=>{str=`${str}${this.translatePermission(binaryArray,permission)}`})this.permissions=str}//判断hasPermission(permission:Permission){if(!this.permissions){returnfalse}returnthis.permissions[permission]==='1'}
