本文首次发表于华为云社区开发与运营版块,主要是做者在学习上云知识过程当中的经验产出。此次和你们分享的是前端开发过程当中可能使用到的进制知识。还在纠结明明代码一毛同样运行结果就是不对吗?还在为判断文件是否为图片的需求而烦恼吗?还在担心位运算的知识点没掌握吗?跟着个人脚步,一一为您解答!
↑开局一张图,故事全靠编。javascript
故事的开头又是从一个Bug讲起,【WEB前端全栈训练营】的@张辉 大大偶遇了一个代码解析的“惊天大Bug”--a标签中的连接没法正常解析,本来连接中应该存在的斜杠莫名其妙的不见了。经过下载大大提供的示例代码,我开始了个人分析之旅,最终想到了对比文件的进制码来分析缘由,在解决问题之余我也想起了曾经遇到的一些进制相关的知识点,如根据文件十六进制码判断文件类型、根据32位二进制理解位运算……php
从开局的图中,咱们清晰的看到HTML解析出了问题,那到底是怎样的代码会有这个解析错误的问题呢?诚然我四年多的前端职业生涯还没遇到过如此牛啤加握草的代码,首先是写不出来,其次是真的不知道怎么写出来的。不过,做为资深前端Copy攻城狮,既然有现成的代码,稍微运功使出个人“CV大法”信手拈来。
解析正常的代码:html
<a href="https://classroom.devcloud.huaweicloud.com/home">首页</a>
解析错误的代码:前端
<a href="https://classroom.devcloud.huaweicloud.com/home">首页</a>
Are you you kidding me?你个糟老头子坏的很,这不是一毛同样的代码吗?不用怀疑,的确不是同样的代码,只是长得像而已。起初,我也觉得是同样的,可是解析出了的结果倒是不同的,眼见不必定为实啊,感受文件究竟是什么样的内容,也许只有机器才懂,毕竟在它的内心。我使用了VS Code的compare folders(文件对比)功能对比了这两行代码,发现的确不同。因而安装了hexdump插件对比了文件编码,结果然相大白。vue
若是说上图还不够明显,那么下图足以说明一切!实际上是空格致使的,我估计是不当心复制到了不正确的空格,致使了在编辑器上肉眼没法观察到,但在浏览器中可以正常解析出来为nbsp;,多么熟悉的空格啊!因此执行一下格式化代码,问题就迎刃而解了!java
丢失的斜杠终于找到了,起初觉得是a标签的问题,看来错怪它了,真正带走斜杠的原来是编码为C2 A0的空格,像极了翻转的故事情节。react
经过上面的分析,我又想到了一个文件上传的问题,做为WEB全栈开发工程师的你,也许会遇到文件上传的需求,咱们知道光靠后缀名是没法判断文件的类型的,一个以.jpg
结尾的文件有多是恶意木马文件,或者会遇到用户上传的是.jpg
结尾的文件却显示失败。其实最主要的缘由是单纯截取文件名称后缀来获取图片格式的方式是不许确的,由于后缀名是能够修改的,记得之前常常将.avi
结尾的文件改成.txt
来掩饰我心里的躁动。那文件头标识长啥样呢?能够参考filesignatures.net,好比JPG的文件头标识为FF D8 FF E0,咱们能够用VS Code打开一张正经的JPG图片和TXT打包成RAR修改成JPG的文件进行对比。算法
图中标出位置为文件头标识,理论上无论文件名后缀怎么改,文件编码都是最开始建立的那种格式。这也意味着咱们不能经过新建文本文件改成.js文件的方式来新建JavaScript文件,表面上咱们看不出什么问题,其实内部已经出了大问题!segmentfault
因而文件上传时的文件类型校验能够这么写(出处:Node.JS 识别图片类型):数组
function getImageSuffix(fileBuffer) { // 将上文提到的 文件标识头 按 字节 整理到数组中 const imageBufferHeaders = [ { bufBegin: [0xff, 0xd8], bufEnd: [0xff, 0xd9], suffix: '.jpg' }, { bufBegin: [0x00, 0x00, 0x02, 0x00, 0x00], suffix: '.tga' }, { bufBegin: [0x00, 0x00, 0x10, 0x00, 0x00], suffix: '.rle' }, { bufBegin: [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a], suffix: '.png' }, { bufBegin: [0x47, 0x49, 0x46, 0x38, 0x39, 0x61], suffix: '.gif' }, { bufBegin: [0x47, 0x49, 0x46, 0x38, 0x37, 0x61], suffix: '.gif' }, { bufBegin: [0x42, 0x4d], suffix: '.bmp' }, { bufBegin: [0x0a], suffix: '.pcx' }, { bufBegin: [0x49, 0x49], suffix: '.tif' }, { bufBegin: [0x4d, 0x4d], suffix: '.tif' }, { bufBegin: [0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x20, 0x20], suffix: '.ico' }, { bufBegin: [0x00, 0x00, 0x02, 0x00, 0x01, 0x00, 0x20, 0x20], suffix: '.cur' }, { bufBegin: [0x46, 0x4f, 0x52, 0x4d], suffix: '.iff' }, { bufBegin: [0x52, 0x49, 0x46, 0x46], suffix: '.ani' } ] for (const imageBufferHeader of imageBufferHeaders) { let isEqual // 判断标识头前缀 if (imageBufferHeader.bufBegin) { const buf = Buffer.from(imageBufferHeader.bufBegin) isEqual = buf.equals( //使用 buffer.slice 方法 对 buffer 以字节为单位切割 fileBuffer.slice(0, imageBufferHeader.bufBegin.length) ) } // 判断标识头后缀 if (isEqual && imageBufferHeader.bufEnd) { const buf = Buffer.from(imageBufferHeader.bufEnd) isEqual = buf.equals(fileBuffer.slice(-imageBufferHeader.bufEnd.length)) } if (isEqual) { return imageBufferHeader.suffix } } // 未能识别到该文件类型 return '' }
位运算也就是按位操做符,包含&(按位与)
、|(按位或)
、^(按位异或)
、~(按位非)
、<<(左移)
、>>(有符号右移)
、<<<(无符号右移)
。照搬下MDN的解释:
运算符 | 用法 | 描述 |
---|---|---|
按位与( AND) | a & b | 对于每个比特位,只有两个操做数相应的比特位都是1时,结果才为1,不然为0。 |
按位或(OR) | a "s" b | 对于每个比特位,当两个操做数相应的比特位至少有一个1时,结果为1,不然为0。 |
按位异或(XOR) | a ^ b | 对于每个比特位,当两个操做数相应的比特位有且只有一个1时,结果为1,不然为0。 |
按位非(NOT) | ~ a | 反转操做数的比特位,即0变成1,1变成0。 |
左移(Left shift) | a << b | 将 a 的二进制形式向左移 b (< 32) 比特位,右边用0填充。 |
有符号右移 | a >> b | 将 a 的二进制表示向右移 b (< 32) 位,丢弃被移出的位。 |
无符号右移 | a >>> b | 将 a 的二进制表示向右移 b (< 32) 位,丢弃被移出的位,并使用 0 在左侧填充。 |
按位操做符操做数字的二进制形式,返回的依然是标准的JavaScript数值,举个栗子:
14 & 9 // 8 14 | 9 // 15 1 << 4 // 16
底层的运算过程多是这样的:先将数字转换为不骂形式的有符号32位整数;如:
14 (base 10) = 00000000000000000000000000001110 (base 2) 9 (base 10) = 00000000000000000000000000001001 (base 2) 1 (base 10) = 00000000000000000000000000000001 (base 2)
进行位运算的时候根据运算规则处理0和1,如按位与&表示相应比特位都为1时结果才为1不然为0;对比上面的数据,前28为都为0,除了29位有两个1结果为1,其余位结果都为0,因此等于00000000000000000000000000001000,就是十进制的8;如按位或的运算规则是至少有一个1时结果为1不然为0,对比上面的数据,前28为依旧都为0,后面四位所有为1因此等于00000000000000000000000000001111,就是十进制的15。如对1进行左移4比特位计算,获得00000000000000000000000000010000结果为16。JavaScript中的进制转换能够用toString和parseInt。
说了那么多,那这个位运算有什么用呢?其实前端三大框架中Vue.js和React源码中都有用到位运算。基本上涉及到状态组合的场景,均可以用位运算中的左移来简化逻辑;最多见的应用之一就是权限控制,具体实践可参考JavaScript 中的位运算和权限设计。另外在一些算法题中,咱们也能用到位运算来解题,如只出现一次的数字II (Single Number II)的一种解法:
/** * @param {number[]} nums * @return {number} */ var singleNumber = function(nums) { let a = 0; let b = 0; for (let i = 0; i < nums.length; i++) { a = a ^ nums[i] & ~b; b = b ^ nums[i] & ~a; } return a };
位运算的其余应用可参考前端笔记-位运算,涉及到的场景有取整、判断奇偶、RGB进制转换等。
本来觉得能紧扣中心思想来阐述主题,结果到最后也不知道本身在讲啥,前端路漫漫,慢慢上下求索,至于全干攻城狮,感受自身目前的水平能作好Copy攻城狮就已经不错了。几年前就注册了一个公众号,惋惜一直没运营起来,同期的小伙伴不少都是流量主了,更是造成了我的影响力。而我,继续努力吧!
![]() |
---|