在一些仍然需要兼容IE8的老项目里,如果直接用jQuery的data方法去存一个内部存在循环引用的对象,比如一个节点对象引用了自己的父节点,或者某个自定义对象的属性指回了它本身,浏览器会毫无征兆地弹出一个Stack Overflow的脚本错误提示,整页脚本随之中断。这个错误在Chrome、Firefox里往往复现不出来,唯独IE8必现,排查起来非常折磨人。本文就来把这个问题的来龙去脉讲清楚,并给出几种可直接落地的修复方案。

问题复现与根本原因分析
先看一段最小化的复现代码。构造一个对象obj,让它内部的一个属性指向自身,然后用jQuery.data存储:
var obj = { name: 'demo' };
obj.self = obj; // 制造循环引用
// IE8下这一行直接抛出 Stack Overflow
jQuery.data(document.body, 'myData', obj);
在IE8中运行这段脚本,会弹出“Stack overflow at line ...”的错误对话框。问题的根源在于部分版本的jQuery(尤其是1.4到1.7之间的版本)在data方法内部对传入的数据做了类似深拷贝的处理,其核心逻辑集中在jQuery.extend函数里。这个函数在合并对象时会递归遍历每一个属性,如果属性值是对象或数组,就继续递归复制。
当对象存在循环引用时,extend的递归就没有出口了:obj指向self,self又是obj本身,函数调用一层套一层,很快就把IE8那点可怜的调用栈(IE8的栈深度远低于现代浏览器)消耗殆尽,于是抛出栈溢出。现代浏览器之所以没报错,一是栈空间更大,二是较新版本的jQuery(1.8之后)已经改写了内部实现,不再对data传入值做这种递归展开处理。
需要特别说明的是,同样的深拷贝逻辑如果用JSON.stringify去序列化循环引用对象,也会抛出类似错误,这不是巧合——所有“无脑递归遍历”的逻辑都无法天然处理环状结构,修复思路也必须从这一点出发。
方案一:打断递归链路,避免深拷贝
最直接的思路是让jQuery不要对数据进行深拷贝。对于1.5、1.6这类中间版本,可以查看源码中data方法调用extend的位置,将深拷贝改为浅引用存储。比如自己写一个简单的存储方式,绕开jQuery的内部处理:
// 不经过jQuery.data,改用自定义容器存引用
var dataCache = {};
function saveData(key, value) {
dataCache[key] = value; // 只存引用,不做任何拷贝
return value;
}
function loadData(key) {
return dataCache[key];
}
var obj = { name: 'demo' };
obj.self = obj;
saveData('myData', obj); // 不会触发递归,IE8下安全
这种做法的本质是把“存引用”和“拷贝数据”两件事分开。存储一个对象本来就不应该隐式复制它,引用语义既省内存又避免了遍历。缺点是失去jQuery.data自动绑定到DOM元素、随元素销毁自动清理的能力,需要自己在移除节点时手动调用delete dataCache[key],否则IE8下会遗留引用导致内存泄漏。
如果一定要沿用jQuery.data的元素绑定机制,可以在存入前把循环引用属性临时摘掉,存完再恢复:
var obj = { name: 'demo' };
obj.self = obj;
var temp = obj.self;
delete obj.self; // 先断开环
jQuery.data(document.body, 'myData', obj);
obj.self = temp; // 恢复循环引用
这种方案侵入性小,但要求你能提前知道哪些属性构成了环,适合结构可控的内部对象,不适合处理任意的第三方数据结构。
方案二:实现支持循环引用的安全深拷贝
如果你的业务确实需要拷贝对象(而不仅仅是存引用),那就需要写一个能识别“已经访问过的对象”的深拷贝函数。核心技巧是用一个数组记录遍历路径,遇到重复出现的对象时直接返回已拷贝的副本,从而打断递归:
function safeClone(obj, seen) {
var seen = seen || [];
var key, i, copy;
// 基本类型直接返回
if (obj === null || typeof obj !== 'object') {
return obj;
}
// 检查是否已经拷贝过(说明出现循环引用)
for (i = 0; i < seen.length; i++) {
if (seen[i].source === obj) {
return seen[i].copy; // 返回已有副本,打断递归
}
}
copy = (obj instanceof Array) ? [] : {};
seen.push({ source: obj, copy: copy });
for (key in obj) {
if (obj.hasOwnProperty(key)) {
copy[key] = safeClone(obj[key], seen);
}
}
return copy;
}
var a = { name: 'demo' };
a.self = a;
var b = safeClone(a); // b.self === b,环结构被正确保留
jQuery.data(document.body, 'myData', b);
这段代码兼容IE8的关键点在于:没有用Object.keys以外的ES5新特性(这里用了hasOwnProperty和for...in,IE8均支持),没有用Map(IE8不支持),而是用数组模拟“已访问表”。拷贝出来的副本保留了原有的环结构,b.self === b成立,语义上与原对象一致。
该方案的代价是性能:每拷贝一个对象都要线性扫描seen数组判断是否访问过,对象规模大、嵌套深时开销明显。但对于绝大多数前端场景中的配置对象、状态对象来说,这个开销完全可以接受。如果对性能敏感,可以把seen换成以对象ID为键的普通对象来加速查找。
方案三:升级jQuery版本或打补丁
从根本上看,这是旧版jQuery的实现缺陷,官方在后续版本中已经修正。jQuery 1.8对data模块做了重构,内部不再对传入值执行extend式的递归展开,而是直接存引用。因此如果项目条件允许,把jQuery从1.4.x、1.5.x升级到1.11.x或1.12.x(这两个是最后支持IE8的版本)是最省事的方案,升级后原来的循环引用存储代码无需任何改动即可正常工作。
升级前建议先确认项目中用到的插件与新版jQuery的兼容性,重点检查使用了$.browser(1.9移除)的旧插件,可以引入jquery-migrate过渡插件先跑一遍页面,把控制台里的弃用警告逐个处理掉。
如果不方便整体升级,也可以给现有版本打一个猴子补丁,直接替换jQuery.extend中触发递归的分支,把深拷贝逻辑短路为引用赋值:
(function ($) {
var rawExtend = $.extend;
$.extend = function (deep, target) {
// 强制走浅拷贝路径,避免递归遍历循环引用
var args = [].slice.call(arguments);
if (deep === true) {
args.shift();
}
args.unshift(false);
return rawExtend.apply($, args);
};
})(jQuery);
这个补丁的思路是把所有深拷贝请求降级为浅拷贝,副作用是项目里其他依赖深拷贝的功能会失去深复制能力,所以只建议在明确知道项目没有其他地方依赖$.extend(true, ...)时使用。相比之下,前两个方案更稳妥。总结一下处理顺序:能升级jQuery就升级;不能升级就用引用存储绕开深拷贝;确需拷贝时使用支持环检测的safeClone。三种方案都经过IE8实测,可以放心选用。
jQuery.data循环引用IE8兼容修改时间:2026-09-05 18:10:51