JavaScript如何正确复制对象?解决展开运算符浅拷贝问题及高效深克隆方法
为什么修改拷贝对象的数组会影响原对象?以及JS高效深克隆方法
这是前端开发里非常常见的浅拷贝陷阱,我来给你一步步拆解清楚:
问题根源:展开运算符是浅拷贝
你用{ ...obj1 }复制对象时,只会复制对象的第一层属性:
- 对于
a、b这类基本类型(字符串),是直接复制它们的值,所以obj2的a和obj1的a是完全独立的; - 但对于
c这个数组(引用类型),展开运算符复制的是数组的内存引用——也就是说obj2的c和obj1的c指向的是同一个数组。所以你修改obj2.c[1]时,其实是在修改那个共享的数组,自然原对象obj1的c也会跟着变。
解决你的示例问题:手动拷贝引用类型属性
如果只是处理你这个简单的对象,只需要单独拷贝数组属性,让它生成新的引用:
let obj1 = { a:'hello', b:'hi', c: [1,2,3] } let obj2 = { ...obj1, c: [...obj1.c] // 这里用展开运算符复制数组,创建一个全新的数组 } obj2.c[1] = 44 console.log(obj1.c) // [1,2,3],原数组不受影响 console.log(obj2.c) // [1,44,3],只有拷贝后的数组被修改
如果对象里还有更深层次的嵌套对象,那就要用到深克隆了。
JavaScript中最高效的深克隆方法
1. 优先使用原生API:structuredClone()
这是ES2022引入的原生深克隆方法,也是现在最推荐的方案:
- 性能优秀,比大部分手写递归函数效率高;
- 支持多种类型:普通对象、数组、Date、Map、Set、TypedArray,甚至能处理循环引用(比如对象里引用自身的情况);
- 使用简单:
const obj1 = { a:'hello', b:'hi', c: [1,2,3], d: new Date() } const deepClonedObj = structuredClone(obj1) deepClonedObj.c[1] = 44 deepClonedObj.d.setDate(10) console.log(obj1.c) // [1,2,3],原数组不变 console.log(obj1.d) // 原日期对象不受影响
唯一需要注意的是,它不支持克隆函数、Symbol属性,而且旧浏览器(比如IE)完全不支持,但现代浏览器(Chrome、Firefox、Edge等)都已经支持了。
2. 手写递归深克隆函数(兼容旧环境或特殊需求)
如果需要兼容旧环境,或者要处理函数这类structuredClone不支持的类型,可以自己写递归函数,记得处理循环引用避免死循环:
function deepClone(obj, cache = new WeakMap()) { // 基本类型直接返回 if (obj === null || typeof obj !== 'object') return obj; // 处理特殊引用类型 if (obj instanceof Date) return new Date(obj.getTime()); if (obj instanceof RegExp) return new RegExp(obj.source, obj.flags); // 处理循环引用:如果已经克隆过这个对象,直接返回缓存的克隆结果 if (cache.has(obj)) return cache.get(obj); // 创建数组或对象的克隆容器 const clone = Array.isArray(obj) ? [] : {}; // 缓存当前对象,避免循环引用 cache.set(obj, clone); // 遍历并递归克隆所有属性 for (const key in obj) { if (obj.hasOwnProperty(key)) { clone[key] = deepClone(obj[key], cache); } } return clone; }
这个函数可以处理大部分场景,包括循环引用,还能自定义处理特殊类型。
3. 不推荐:JSON.parse(JSON.stringify(obj))
虽然这个方法看起来简单,但有很多致命缺点:
- 会丢失函数、undefined、Symbol类型的属性;
- 日期对象会被转成字符串,无法保留日期类型;
- 正则表达式会被转成空对象;
- 无法处理循环引用,会直接报错;
所以只适合处理非常简单的、没有特殊类型的对象,不推荐作为通用深克隆方案。
内容的提问来源于stack exchange,提问作者Gustavo Hiroki Nakagawa
相关产品推荐
相关产品推荐

