BOM 是 Unicode 字元 U+FEFF,某些編輯器和編碼器會將它附加在文字檔案的最前面;當這些位元組被解碼到瀏覽器的文字輸入區時,JavaScript 字串的第一個 UTF-16 程式碼單元可能等於十六進位的 FEFF,即使畫面上並沒有顯示任何字元。BOM 移除工具是一個刻意設計得很狹隘的工具,它只檢查 input.charCodeAt(0) 是否等於 FEFF,如果相符,就回傳 input.slice(1),剛好移除那一個程式碼單元,不做其他任何事。內部、尾端以及第二個前置的 U+FEFF 字元都會被保留,轉換完全在目前的瀏覽器分頁中執行,不會上傳文字,輸入和輸出各自適用 200,000 個 UTF-16 程式碼單元的上限。狀態列會回報究竟移除了剛好一個前置 BOM,還是沒有找到前置 BOM,而且即使輸出為空,結果面板也會顯示,以免把只含一個 BOM 的輸入縮短為空字串誤判為失敗。本文會說明這個工具做了什麼、刻意不處理什麼,以及在可信任的解碼器暴露出干擾剖析器、shebang 或字串比對的前置 U+FEFF 時,什麼時候該選用它。

how to remove bomber side shields
如何移除 Bomber 側護板或前置 BOM

Bomber 側護板與文字 BOM:你指的是哪一個?

「Bomber 側護板」通常指的是 Bombeman 和 MachineartMoto 等品牌販售的機車改裝側板,它們用螺絲鎖在油箱或引擎兩側。這類護板是實體的硬體,用螺絲或夾具固定,拆除時需要 Torx 或六角扳手,並且能夠接觸到車輛。本文不是在講這種護板,光是把東西貼到瀏覽器裡也沒辦法幫你處理鈑金。

如果你會搜尋這個詞,是因為剖析器、編譯器或 JSON 驗證工具一直拒絕一個檔頭有看不見字元的檔案,那麼你遇到的問題幾乎肯定是另一回事:一個 Unicode 位元組順序記號(縮寫 BOM),位於你文字的第零個位置。這兩個詞聽起來有點像,但其實毫無關聯。第一個是機車上的鈑金護板。第二個是 Unicode 中單一的碼點 U+FEFF,某些文字編輯器和編碼器會把它附加在檔案前面當作簽章。一旦這個檔案被載入成 JavaScript 字串,剩下的唯一問題就是該字串的第一個程式碼單元是否等於 FEFF。

在瀏覽器文字輸入區中,前置 BOM 到底是什麼

根據 WHATWG 編碼活標準和 Unicode 標準,BOM 是位於資料流開頭的一個碼點。同一概念的 UTF-8 位元組簽章是三位元組序列 EF BB BF,而 UTF-16 使用 FF FE 或 FE FF,UTF-32 也有其對應的簽章。等到這些位元組經過解碼器送達瀏覽器的文字輸入區時,位元組已經消失了。剩下的是 JavaScript 字串,唯一可以偵測到的訊號就是第一個程式碼單元是否等於十六進位的 FEFF。

這項區別為瀏覽器端工具的能力劃下一條硬界線。文字輸入區裡放的是解碼後的字元。它無法知道原始檔案是帶有 EF BB BF 簽章的 UTF-8、帶 FF FE 的 UTF-16 LE、帶 FE FF 的 UTF-16 BE,還是根本沒有簽章的純 ASCII。BOM 移除工具只把這一個事實當作它的全部規則,不會嘗試推測來源檔案的任何資訊。如果想更深入了解瀏覽器端解碼中的同一概念,「在瀏覽器中從文字移除前置 BOM」的指南涵蓋了關於解碼器與複製貼上工作流程的另一個角度。

來源訊號磁碟上的位元組在瀏覽器文字輸入區中顯示為BOM 移除工具能否偵測
UTF-8 含 BOMEF BB BF前置 U+FEFF可以
UTF-16 LE 含 BOMFF FE前置 U+FEFF可以
UTF-16 BE 含 BOMFE FF前置 U+FEFF可以
UTF-32 LE 含 BOMFF FE 00 00前置 U+FEFF可以
UTF-8 不含 BOM檔案的第一個字元不行
純 ASCII檔案的第一個字元不行

如何從貼上的文字中精準移除一個前置 BOM

只有在可信任的解碼器或編輯器暴露出你確定不需要的前置 U+FEFF 之後,才使用 BOM 移除工具。這個工具不會提醒你邊界情況;它只會執行它單一、保守的動作,然後把結果顯示給你。完整流程有三個步驟。

  1. 把已經解碼的文字貼進輸入區,如果字串中存在看不見的前置 U+FEFF,也要一併貼上。
  2. 執行移除工具,閱讀狀態列以確認究竟移除了剛好一個前置 U+FEFF,還是沒有找到前置 U+FEFF。
  3. 檢視完整的輸出,包括行尾符號以及任何刻意保留的後置 U+FEFF 內容,然後在本機複製結果。

編輯或清空輸入會立即清除先前的輸出、狀態、錯誤和剪貼簿狀態,所以舊的結果不會殘留到下一次貼上。如果工具回報已移除一個前置 BOM,輸出就是輸入扣掉第一個程式碼單元。如果回報沒有找到前置 BOM,輸出就與輸入逐字元相同。因為唯一的操作就是一次切片,你可以在另一個工具中對輸入和輸出做 diff,確認究竟有沒有改變。

BOM 移除工具刻意保留的項目

這個實作刻意不會對輸入呼叫 trim、trimStart、replaceAll 或全域正規表達式。這一個設計決定帶來幾個值得在貼上之前理解的明顯後果,而它們都源自同一條規則:位置就是全部的規則。

  • 位於字串中間的 U+FEFF 會保留在中間,即使它看起來像個多餘的簽章。
  • 換行之後的 U+FEFF 會被當作內容保留,不會被摺疊掉。
  • 位於字串尾端的 U+FEFF 會被保留,所以類 BOM 的尾端字元在操作後仍會存在。
  • 開頭連續兩個 U+FEFF 碼點是刻意處理的:第一個會被當作可能的簽章移除,第二個則保留為初始內容,因為 Unicode 允許這種模式,用來表示一個前置的不換行零寬空格。再執行一次工具會接著移除當時變成前置的第二個 U+FEFF,所以重複操作前請務必先閱讀狀態。
  • 空輸入是有效的「無變更」情況。結果面板仍然會顯示,所以不會把看不到的輸出誤判為執行失敗或沒執行。
  • 只含一個前置 U+FEFF 的輸入會產生有效的空輸出,狀態列會說明已移除一個 BOM,輸出長度回報為零個 UTF-16 程式碼單元。

Unicode 的指引指出,非起始位置的 U+FEFF 歷來帶有不換行零寬空格的語意,即使新文字中偏好使用 U+2060 WORD JOINER。因此移除所有出現的 U+FEFF 會造成破壞,這也是為什麼 BOM 移除工具狹隘的切片是安全的預設行為。

什麼時候不該使用 BOM 移除工具

這個工具的狹隘性是特色,但也定義了它的限制。下列工作明確超出範圍,不管你按多少次都不會靠 BOM 移除工具解決。

  • 從貼上的文字推測原始檔案編碼。文字輸入區已經不再包含位元組,工具只看得見字元。
  • 轉換 UTF-16 位元組序列、修復亂碼(mojibake),或驗證 UTF-8。轉換只在字元層級進行。
  • 移除串接文件中所有類 BOM 或零寬字元。每次點擊只會動到第一個程式碼單元。
  • 修剪尾端空白、統一 CRLF 或 LF 行尾符號,或套用任何一種 Unicode 正規化形式。
  • 處理超過 200,000 個 UTF-16 程式碼單元的輸入。超過上限的下一個程式碼單元會在轉換執行前被拒絕,而超過上限的錯誤不會讓舊的成功結果殘留顯示。

如果你能控制檔案的載入方式,先挑一個 BOM 政策符合需求的解碼器或編輯器,再把任何東西貼到瀏覽器。如果你已經貼上,而且不確定前置字元是否真的不需要,請在覆寫原始檔案之前,用 diff 工具比對輸出和輸入。根據 WHATWG 編碼活標準,各解碼器暴露或剝除前置 BOM 的做法不同,前置 BOM 究竟算不算「錯誤」,幾乎總是取決於文字的使用者,而不是位元組本身。

限制、錯誤與空輸出的情況

輸入和輸出各自獨立以 200,000 個 UTF-16 程式碼單元為上限,而且這個界線會被嚴格執行:剛好等於上限的輸入會被接受,下一個程式碼單元會被拒絕,輸出也適用相同的規則。因為轉換只會移除零個或一個程式碼單元,有效的可產出輸入在處理過程中不會膨脹,獨立的輸出檢查是一條明確的不變式,而不是例行把關。尺寸檢查失敗時,先前的結果、錯誤、摘要、移除旗標,以及任何待處理的剪貼簿訊息都會被清除,所以一次工具執行不會意外地同時顯示舊的輸出和新的錯誤。

摘要列會區分三種情況。「已移除一個前置 BOM」會在 input.charCodeAt(0) 等於十六進位 FEFF,且輸出比輸入少一個程式碼單元時出現。「找不到前置 BOM」會在第一個程式碼單元是其他任何東西(包括空輸入)時出現。輸出長度以 UTF-16 程式碼單元為單位回報,這是 JavaScript 字串內部使用的單位,也是把代理配對算成兩個的單位。剪貼簿存取是非同步的,並由世代識別碼保護,所以延遲到的授權回應無法還原過期的「已複製」狀態;而被拒絕的要求則會保留完整的唯讀輸出供手動選取。