网站如何分辨 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 结果和递归上限中读出少数几个引擎桶。它无法把你单独挑出来,却能说明浏览器的自述与行为是否矛盾。想看看页面眼中的你的引擎,可以运行指纹检测或内核检测。


