網站如何分辨 V8、SpiderMonkey 與 JavaScriptCore:呼叫堆疊、錯誤訊息、toString 與 Math 精度,及其能證明什麼。
User-Agent 字串只是一種「宣告」。真正在執行頁面程式碼的是 JavaScript 引擎,而它的實作細節會透過一般的標準 API 洩漏出來。這正是 JS 引擎訊號的價值所在:它不是一個會跟著你跨站的高熵識別碼,而是一台針對瀏覽器「自稱身分」的測謊儀。如果你想先了解 JavaScript 引擎是什麼、與渲染引擎如何搭配,請先讀 渲染引擎 vs JavaScript 引擎;本文直接進入具體技術。
核心要點
- 三大引擎涵蓋幾乎所有真實瀏覽器:V8(Chrome、Edge 等 Chromium 系瀏覽器)、SpiderMonkey(Firefox)、JavaScriptCore(Safari,以及實際上 iOS 上幾乎所有瀏覽器)。
- 引擎特徵來自語言規範留白的地方:呼叫堆疊格式、內建錯誤訊息、原生函式原始碼文字,以及部分
Math函式的精度。 - 這個訊號很粗:它只能把訪客分進少數幾個桶(再加上版本時代差異),所以單獨使用只能辨識「引擎」,不能辨識「某個人」。
- 它真正的價值在於一致性校驗:自稱 Safari 卻按 V8 方式執行的瀏覽器,向頁面講了兩個互相矛盾的故事。
- BrowserInsight 自己的一致性檢測,在其他引擎訊號無法判斷時,會回退到
Error().stack的格式來判定引擎。
引擎為何會暴露自己
ECMAScript 規範規定語言「必須做什麼」,卻沒有規定每一個可觀察細節「必須長什麼樣」。規範沉默或明確標註為「由實作定義」的地方,各個引擎做了各自的選擇,而且這些選擇多年來保持穩定,因為網站逐漸依賴了它們。整個過程不需要任何特殊權限:指令碼讀取一段字串,與各引擎已知的輸出對照,就得到了答案。
技術一:Error.stack 的形狀
Error.prototype.stack 不屬於標準,所以各家才會不同。V8 的字串以錯誤名稱和訊息開頭,每個堆疊框寫成 at functionName (url:line:column);SpiderMonkey 與 JavaScriptCore 沒有這行標頭,堆疊框寫成 functionName@url:line:column。這兩個非 V8 引擎之間,則可以靠匿名框、欄號寫法等更細的差異來區分,這些差異隨版本變化,需要持續複核,不能寫死。
V8 還首創了兩個配套 API:Error.captureStackTrace 和 Error.stackTraceLimit。MDN 將 captureStackTrace 標註為非標準,其相容性表是查看哪些引擎後來也加入支援的地方。這類 V8 風味擴充是否存在,是第二個獨立的提示。
技術二:內建錯誤訊息文字
在三個引擎裡犯同一個錯誤(呼叫一個不是函式的值、讀取 null 的屬性),會得到三句不同的話,因為規範只規定了錯誤的「類型」,沒有規定訊息。MDN 的 JavaScript 錯誤參考 列出了許多錯誤在各引擎下的訊息變體。頁面可以捕獲例外、讀取 error.message,再與一張小對照表比對。同一引擎不同版本的文字也會變,因此嚴謹的檢測只把它當作「引擎家族」的證據,而不是精確版本。
技術三:原生函式的 Function.prototype.toString
對 Array.prototype.push 這類內建函式呼叫 Function.prototype.toString,會得到形如 function push() { [native code] } 的文字。現代規範固定了大致形態,但空白與格式仍有空間,字串本身及其長度歷史上在不同引擎之間有所差異。指令碼可以拿幾個內建函式的輸出,與自稱的瀏覽器應有的結果對照。這個探針還有第二個用途:被 JavaScript 包裝函式取代的原生函式不再印出 [native code],所以測謊類工具很依賴它。
技術四:邊界處的 Math 精度
ECMAScript 規範只要求 Math.sin、Math.exp、Math.pow 等函式回傳「由實作近似」的結果。MDN 的 Math 參考 指出,這類函式的精度取決於具體實作。因此引擎(有時還包括 CPU 或作業系統的數學函式庫)在特殊參數下可能在最後幾位上出現分歧。由於這種尾數行為同時取決於數學函式庫和引擎,應把它視為把訪客分群的輔助訊號,而不是乾淨的「每引擎一個標籤」。本文不列出具體數字,它們會隨版本和平台變化。
技術五:遞迴深度上限
不斷遞迴直到引擎放棄,再捕獲它拋出的例外。V8 與 JavaScriptCore 拋出 RangeError,訊息是超出最大呼叫堆疊大小;SpiderMonkey 拋出的是它非標準的 InternalError,訊息為「too much recursion」(見 MDN 的 too much recursion 條目)。錯誤類型和措辭都是粗略的引擎特徵。而報錯前達到的深度取決於堆疊框大小、堆疊容量和裝置,更適合當作建置與環境的大致線索,而不是精確的引擎識別,它也是這份清單裡最不穩定的探針。
為什麼難以偽造
基於 Chromium 的指紋瀏覽器可以改寫 User-Agent、Client Hints 和 navigator 屬性,但它跑的仍然是 V8。要讓 V8 輸出 SpiderMonkey 或 JavaScriptCore 風格的呼叫堆疊格式、錯誤訊息、toString 文字和 Math 結果,就得修補大量彼此獨立的行為,而每一個被修補的內建函式本身,都可能因原型層面的改動而被發現。這也是引擎檢測與 CreepJS 式測謊分析 搭配得很好的原因;偽裝設定檔與真實瀏覽器之間的差距(見指紋瀏覽器 vs 真實瀏覽器),往往出現在跨訊號的不一致上,而不是某一個欄位。本文描述的是檢測,不是規避指南。
用在哪裡
正當用途是一致性校驗。機器人與風控系統會把連線「自稱」的瀏覽器(參見如何偵測 User-Agent 偽造)與實際執行程式碼的引擎做比較,作為機器人偵測和無頭瀏覽器偵測中的眾多輸入之一。同樣的思路在版本區間上也適用:基於特性的版本偵測判斷引擎「有多新」,而上述技術判斷的是它「是哪個引擎」。
BrowserInsight 實際檢測了什麼
坦白說兩件事。/fingerprint-check、/bot-detection 和 /vpn-check 背後的一致性檢測,在其他訊號無法判斷時,會回退到 Error().stack 格式來判定引擎。核心檢測則另行根據特性支援情況估算引擎版本。我們不探測錯誤訊息、Math 精度或 toString 長度,而且一切都在你的瀏覽器本機執行,不會向伺服器傳送任何資料。
常見問題
網站能憑 JavaScript 引擎認出我嗎? 單靠它不行。引擎只把訪客分成幾個很大的群體,對唯一指紋的貢獻微乎其微。它的價值在於檢驗你的其他宣告是否自洽。
JavaScript 引擎不同就意味著瀏覽器不同嗎? 通常是。Chrome、Edge 和 Brave 都執行 V8,Firefox 執行 SpiderMonkey,Safari 與 iOS 上幾乎所有瀏覽器執行 JavaScriptCore。同一家族的瀏覽器僅憑引擎無法區分。
這些差異是永久的嗎? 不是。各引擎會在版本之間調整訊息文字、堆疊格式和數學實作,標準化工作也可能消除部分差異。任何基於它們的檢測都需要持續維護。
Error.stack 是標準嗎?
它被廣泛實作但並非標準,MDN 對此有記錄,這也是它的格式能成為引擎訊號的原因。
總結
JS 引擎指紋是一個小而誠實的訊號:從呼叫堆疊、錯誤訊息、原生函式原始碼、Math 結果和遞迴上限中讀出少數幾個引擎桶。它無法把你單獨挑出來,卻能說明瀏覽器的自述與行為是否矛盾。想看看頁面眼中的你的引擎,可以執行指紋檢測或核心檢測。


