前端必知必会:JavaScript 对象与数组克隆的 7 种姿势,从浅入深一网打尽!

大家好,我是鱼樱!!!

关注公众号【鱼樱AI实验室】持续每天分享更多前端和AI辅助前端编码新知识~~喜欢的就一起学反正开源至上,无所谓被诋毁被喷被质疑文章没有价值~~~坚持自己观点

写点笔记写点生活~写点经验。

在当前环境下,纯前端开发者可以通过技术深化、横向扩展、切入新兴领域以及产品化思维找到突破口。

为什么需要克隆?

JavaScript 中直接赋值对象或数组时,传递的是引用地址而非独立副本。修改克隆对象会影响原对象,导致难以追踪的 Bug。掌握正确的克隆姿势是前端开发的必备技能!


一、浅拷贝:仅复制第一层属性

适用场景:简单对象、无嵌套结构、无需修改子属性。

1. Object.assign()

javascript 复制代码
const objClone = Object.assign({}, originalObj);
  • 原理:合并对象属性到新对象。

  • 缺点:嵌套对象仍是引用。

  • 示例

    javascript 复制代码
    objClone.name = '新名字';      // 不影响原对象
    objClone.friends.name = '改'; // 原对象的 friends 也会被修改!

2. 展开运算符 ...

javascript 复制代码
const objClone = { ...originalObj };
const arrClone = [ ...originalArr ];
  • 特点 :语法简洁,与 Object.assign() 等效。
  • 注意:同样无法处理嵌套结构。

3. 数组的 slice()

javascript 复制代码
const arrClone = originalArr.slice();
  • 本质:截取数组全部元素生成新数组。
  • 局限:仅适用于数组,且不处理嵌套对象。

二、深拷贝:完全独立副本,断绝所有引用

适用场景:复杂对象、嵌套结构、需要完全隔离修改。

4. JSON.parse(JSON.stringify())

javascript 复制代码
const deepClone = JSON.parse(JSON.stringify(originalObj));
  • 原理:通过 JSON 序列化反序列化生成新对象。
  • 致命缺陷
    • 丢失 undefinedSymbol、函数属性。
    • 无法处理循环引用(如 obj.self = obj)。
  • 适用场景:纯数据对象(无特殊类型)。

5. structuredClone()

javascript 复制代码
const deepClone = structuredClone(originalObj);
  • 特点
    • 浏览器原生 API,支持 undefinedDateRegExp 等。
    • 可处理循环引用(部分浏览器支持)。
  • 注意 :兼容性问题(IE 不支持),检查 Can I Use

6. 手动递归实现

javascript 复制代码
function deepClone(obj, hash = new WeakMap()) {
  if (obj === null || typeof obj !== 'object') return obj;
  if (hash.has(obj)) return hash.get(obj); // 解决循环引用
  const clone = Array.isArray(obj) ? [] : {};
  hash.set(obj, clone);
  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      clone[key] = deepClone(obj[key], hash);
    }
  }
  return clone;
}
  • 优势 :完全可控,可扩展处理特殊类型(如 DateMap)。
  • 优化点
    • 使用 WeakMap 解决循环引用。
    • 增加类型判断处理边界情况。

三、方案对比与选型指南

方法 深浅拷贝 处理循环引用 保留特殊类型 性能
Object.assign
JSON 序列化
structuredClone ✔️(部分) ✔️
手动递归 ✔️ ✔️(需扩展) 低(复杂对象)

四、实际开发中的坑与解决方案

  1. 循环引用报错

    • 使用 WeakMap 缓存已拷贝对象(参考手动实现代码)。
  2. 函数属性丢失

    • 手动实现中增加函数处理逻辑:

      javascript 复制代码
      if (typeof obj === 'function') return obj.bind({});
  3. 特殊对象处理(如 Date

    • 在递归中识别类型并重建:

      javascript 复制代码
      if (obj instanceof Date) return new Date(obj);

附上实践案例

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>对象克隆</title>
</head>
<body>
  <script>
    // 定义一个对象
    const obj1 = {
      name: 'obj1',
      age: 18,
      sex: '男',
      friends: {
        name: 'obj2',
        age: 18,
        sex: '男'
      },
      address: undefined // 包含 undefined 的属性 
    }

    // 浅拷贝
    const obj3 = Object.assign({}, obj1);
    obj3.name = 'obj3';
    obj3.friends.name = 'obj4';

    console.log(obj1); // {name: 'obj1', age: 18, sex: '男', friends: {...}, address: undefined}
    console.log(obj3); // {name: 'obj1', age: 18, sex: '男', friends: {...}, address: undefined}
    console.log(obj1 === obj3); // false
    console.log(obj1.friends === obj3.friends); // true
    console.log(obj1.friends.name === obj3.friends.name); // true
    
    // 浅拷贝二 如果是数组可以使用 slice() 方法
    const obj4 = {...obj1};
    obj4.name = 'obj4';
    obj4.friends.name = 'obj5';

    console.log(obj1); // {name: 'obj1', age: 18, sex: '男', friends: {...}, address: undefined}
    console.log(obj4); // {name: 'obj1', age: 18, sex: '男', friends: {...}, address: undefined}
    console.log(obj1 === obj4); // false
    console.log(obj1.friends === obj4.friends); // true
    console.log(obj1.friends.name === obj4.friends.name); // true

    // 克隆对象 - 深度copy Lodash 的 _.cloneDeep() 方法
    // 但从性能和某些特殊对象的处理(例如包含函数、undefined、Symbol 或循环引用的对象 则不适用)角度来看,这种方法并不是最优的深拷贝实现方式
    const obj2 = JSON.parse(JSON.stringify(obj1));

    // 修改克隆对象
    obj2.name = 'obj2';
    obj2.friends.name = 'obj3';

    // 打印对象
    console.log(obj1); // {name: 'obj1', age: 18, sex: '男', friends: {...}, address: undefined}
    console.log(obj2); // {name: "obj2", age: 18, sex: "男", friends: {name: "obj3", age: 18, sex: "男"}}
    console.log(obj1 === obj2); // false
    console.log(obj1.friends === obj2.friends); // false
    console.log(obj1.friends.name === obj2.friends.name); // false

    // 某些现代浏览器支持使用 structuredClone 方法进行深拷贝
    const obj5 = structuredClone(obj1);
    obj5.name = 'obj5';
    obj5.friends.name = 'obj6';

    console.log(obj1); // {name: 'obj1', age: 18, sex: '男', friends: {...}, address: undefined}
    console.log(obj5); // {name: 'obj1', age: 18, sex: '男', friends: {...}, address: undefined}
    console.log(obj1 === obj5); // false
    console.log(obj1.friends === obj5.friends); // false
    console.log(obj1.friends.name === obj5.friends.name); // false

    // 手动实现是否深度拷贝 默认是浅拷贝
    function clone(obj, deep = false) {
      // 如果是基本类型或者 null 直接返回
      if (typeof obj !== 'object' || obj === null) {
        return obj;
      }
      // 如果是数组
      if (Array.isArray(obj)) {
        return deep ? obj.map(item => clone(item, deep)) : obj.slice();
      }
      // 如果是对象
      const result = {};
      for (const key in obj) {
        if (obj.hasOwnProperty(key)) {
          result[key] = deep ? clone(obj[key], deep) : obj[key];
        }
      }
      return result;
    }

    // 浅拷贝
    const obj6 = clone(obj1);
    obj6.name = 'obj6';
    obj6.friends.name = 'obj7';

    console.log(obj1); // {name: 'obj1', age: 18, sex: '男', friends: {...}, address: undefined}
    console.log(obj6,'手动实现浅拷贝'); // {name: 'obj1', age: 18, sex: '男', friends: {...}, address: undefined}
    console.log(obj1 === obj6); // false
    console.log(obj1.friends === obj6.friends); // true
    console.log(obj1.friends.name === obj6.friends.name); // true

    // 深拷贝
    const obj7 = clone(obj1, true);
    obj7.name = 'obj7';
    obj7.friends.name = 'obj8';

    console.log(obj1); // {name: 'obj1', age: 18, sex: '男', friends: {...}, address: undefined}
    console.log(obj7,'手动实现深拷贝'); // {name: 'obj7', age: 18, sex: '男', friends: {name: 'obj8', age: 18, sex: '男'}, address: undefined}
    console.log(obj1 === obj7); // false
    console.log(obj1.friends === obj7.friends); // false
    console.log(obj1.friends.name === obj7.friends.name); // false
  </script>
</body>
</html>

五、总结

  • 简单场景 :优先使用 ...Object.assign()
  • 数据克隆JSON 方法快捷但需注意类型丢失。
  • 生产环境 :使用 structuredClone 或成熟工具库(如 Lodash 的 _.cloneDeep())。
  • 极致控制:手动实现深拷贝,按需扩展。

记住:没有完美的深拷贝方案,只有最适合当前场景的选择!

相关推荐
IT瘾君1 小时前
JavaWeb:Html&Css
前端·html
264玫瑰资源库2 小时前
问道数码兽 怀旧剧情回合手游源码搭建教程(反查重优化版)
java·开发语言·前端·游戏
喝拿铁写前端2 小时前
从圣经Babel到现代编译器:没开玩笑,普通程序员也能写出自己的编译器!
前端·架构·前端框架
HED2 小时前
VUE项目发版后用户访问的仍然是旧页面?原因和解决方案都在这啦!
前端·vue.js
拉不动的猪2 小时前
前端自做埋点,我们应该要注意的几个问题
前端·javascript·面试
王景程2 小时前
如何测试短信接口
java·服务器·前端
安冬的码畜日常3 小时前
【AI 加持下的 Python 编程实战 2_10】DIY 拓展:从扫雷小游戏开发再探问题分解与 AI 代码调试能力(中)
开发语言·前端·人工智能·ai·扫雷游戏·ai辅助编程·辅助编程
烛阴3 小时前
Node.js中必备的中间件大全:提升性能、安全与开发效率的秘密武器
javascript·后端·express
小杨升级打怪中3 小时前
前端面经-JS篇(三)--事件、性能优化、防抖与节流
前端·javascript·xss
清风细雨_林木木3 小时前
Vue开发网站会有“#”原因是前端路由使用了 Hash 模式
前端·vue.js·哈希算法