【Copy攻城獅日誌】斜槓去哪兒了

由一個HTML解析Bug引起的思考

本文首次發表於華爲雲社區開發與運營版塊,主要是做者在學習上雲知識過程當中的經驗產出。此次和你們分享的是前端開發過程當中可能使用到的進制知識。還在糾結明明代碼一毛同樣運行結果就是不對嗎?還在爲判斷文件是否爲圖片的需求而煩惱嗎?還在擔心位運算的知識點沒掌握嗎?跟着個人腳步,一一爲您解答!

start.png

↑開局一張圖,故事全靠編。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 Codecompare folders(文件對比)功能對比了這兩行代碼,發現的確不同。因而安裝了hexdump插件對比了文件編碼,結果然相大白。
compare.pngvue

若是說上圖還不夠明顯,那麼下圖足以說明一切!實際上是空格致使的,我估計是不當心複製到了不正確的空格,致使了在編輯器上肉眼沒法觀察到,但在瀏覽器中可以正常解析出來爲nbsp;,多麼熟悉的空格啊!因此執行一下格式化代碼,問題就迎刃而解了!java

compare2.png

丟失的斜槓終於找到了,起初覺得是a標籤的問題,看來錯怪它了,真正帶走斜槓的原來是編碼爲C2 A0空格,像極了翻轉的故事情節。react

文件頭標識

經過上面的分析,我又想到了一個文件上傳的問題,做爲WEB全棧開發工程師的你,也許會遇到文件上傳的需求,咱們知道光靠後綴名是沒法判斷文件的類型的,一個以.jpg結尾的文件有多是惡意木馬文件,或者會遇到用戶上傳的是.jpg結尾的文件卻顯示失敗。其實最主要的緣由是單純截取文件名稱後綴來獲取圖片格式的方式是不許確的,由於後綴名是能夠修改的,記得之前常常將.avi結尾的文件改成.txt來掩飾我心裏的躁動。那文件頭標識長啥樣呢?能夠參考filesignatures.net,好比JPG的文件頭標識爲FF D8 FF E0,咱們能夠用VS Code打開一張正經的JPG圖片和TXT打包成RAR修改成JPG的文件進行對比。算法

compare3.png

圖中標出位置爲文件頭標識,理論上無論文件名後綴怎麼改,文件編碼都是最開始建立的那種格式。這也意味着咱們不能經過新建文本文件改成.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)

進行位運算的時候根據運算規則處理01,如按位與&表示相應比特位都爲1時結果才爲1不然爲0;對比上面的數據,前28爲都爲0,除了29位有兩個1結果爲1,其餘位結果都爲0,因此等於00000000000000000000000000001000,就是十進制的8;如按位或的運算規則是至少有一個1時結果爲1不然爲0,對比上面的數據,前28爲依舊都爲0,後面四位所有爲1因此等於00000000000000000000000000001111,就是十進制的15。如對1進行左移4比特位計算,獲得00000000000000000000000000010000結果爲16。JavaScript中的進制轉換能夠用toStringparseInt

vueandreact.png

說了那麼多,那這個位運算有什麼用呢?其實前端三大框架中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攻城獅就已經不錯了。幾年前就註冊了一個公衆號,惋惜一直沒運營起來,同期的小夥伴不少都是流量主了,更是造成了我的影響力。而我,繼續努力吧!

微@信@公@衆@號:胡琦
相關文章
相關標籤/搜索