JavaScript对象深浅拷贝及解析

JavaScript对象深浅拷贝及解析

 
下面这段代码使用了 JavaScript 中的对象展开运算符(...),核心作用是创建一个 obj1 的浅拷贝对象 obj2
 

1. 基础用法示例

 
先看一个完整的可运行示例,帮你理解效果:
// 原始对象
const obj1 = {
  name: "张三",
  age: 20,
  hobbies: ["篮球", "游戏"] // 嵌套的引用类型
};

// 浅拷贝 obj1 到 obj2
const obj2 = { ...obj1 };

// 修改 obj2 的基础属性,不会影响 obj1
obj2.name = "李四";
console.log(obj1.name); // 输出:张三(obj1 不受影响)
console.log(obj2.name); // 输出:李四

// 注意:浅拷贝的坑——修改嵌套对象会影响原对象
obj2.hobbies.push("看书");
console.log(obj1.hobbies); // 输出:["篮球", "游戏", "看书"](obj1 被影响了)
console.log(obj2.hobbies); // 输出:["篮球", "游戏", "看书"]

image

2. 关键知识点解释

 
  • 浅拷贝:只复制对象的第一层属性。如果对象里嵌套了数组、另一个对象等 “引用类型”,拷贝的只是它们的内存地址,而非真正的独立数据(这就是上面示例中 hobbies 数组被同时修改的原因)。

与直接赋值的区别

// 直接赋值:obj3 和 obj1 指向同一个内存地址,修改一个都会变
const obj3 = obj1;
obj3.name = "王五";
console.log(obj1.name); // 输出:王五(obj1 被修改)

扩展用法:拷贝时还能新增 / 覆盖属性

const obj2 = { ...obj1, age: 21, gender: "男" };
console.log(obj2.age); // 输出:21(覆盖原属性)
console.log(obj2.gender); // 输出:男(新增属性)

image

3. 如何实现深拷贝(解决嵌套引用问题)

 
如果需要完全独立的拷贝(包括嵌套对象),可以用以下方法:

第一种深拷贝的方法:

 基础版递归深拷贝(核心逻辑)

这个版本只处理普通对象数组,以及基本类型,去掉了特殊类型和循环引用的处理,核心逻辑一目了然,适合理解递归的本质:
/**
 * 基础版递归深拷贝(仅处理普通对象、数组、基本类型)
 * @param {*} obj 要拷贝的对象/数组/基本类型
 * @returns {*} 拷贝后的新值
 */
function deepCloneBasic(obj) {
  // 1. 终止条件:如果是基本类型/null,直接返回(递归出口)
  if (obj === null || typeof obj !== 'object') {
    return obj;
  }

  // 2. 创建空容器:区分数组和普通对象
  let cloneObj = Array.isArray(obj) ? [] : {};

  // 3. 遍历原对象/数组的属性/元素,递归拷贝
  for (let key in obj) {
    // 只遍历自有属性(避免遍历原型链上的属性)
    if (obj.hasOwnProperty(key)) {
      // 核心:对每个值递归调用深拷贝
      cloneObj[key] = deepCloneBasic(obj[key]);
    }
  }

  return cloneObj;
}

// ============== 测试示例 ==============
// 原始对象(包含嵌套对象和数组)
const obj1 = {
  name: "张三",
  age: 20,
  hobbies: ["篮球", "游戏"],
  info: {
    address: "北京"
  }
};

// 深拷贝
const obj2 = deepCloneBasic(obj1);

// 验证:修改拷贝后的对象,原对象不受影响
obj2.name = "李四";
obj2.hobbies.push("看书");
obj2.info.address = "上海";

console.log("原对象 obj1:", obj1);
// 输出:obj1 = { name: '张三', age: 20, hobbies: ['篮球', '游戏'], info: { address: '北京' } }

console.log("拷贝后的 obj2:", obj2);
// 输出:obj2 = { name: '李四', age: 20, hobbies: ['篮球', '游戏', '看书'], info: { address: '上海' } }

以上这版基础的递归拷贝,最主要的是要弄明白在 deepCloneBasic 这个递归深拷贝函数里,每一层递归创建的 cloneObj 为什么不会互相冲突,核心是理解递归过程中变量的作用域问题。

核心原因:函数执行上下文与变量作用域

 
要理解这个问题,关键在于 JavaScript 中函数每次调用都会创建独立的执行上下文,函数内声明的变量(比如 cloneObj)是局部变量,只在当前这次函数调用中有效,不同调用之间互不干扰。
 
结合上面的代码和递归执行过程来拆解:
 

1. 递归调用的执行过程(以 obj1.info 为例)

 
当拷贝 obj1 = { info: { address: "北京" } } 时,递归调用是分层的:
// 第一层调用:处理整个 obj1
deepCloneBasic(obj1) 
  → 创建 cloneObj = {}(空对象,用于存储 obj1 的拷贝)
  → 遍历到 key = "info",值是 { address: "北京" }
  → 调用 deepCloneBasic(obj1.info)  // 第二层递归

  // 第二层调用:处理 obj1.info
  deepCloneBasic(obj1.info)
    → 创建 cloneObj = {}(空对象,用于存储 obj1.info 的拷贝)
    → 遍历到 key = "address",值是 "北京"(基本类型)
    → 直接返回 "北京",赋值给当前层的 cloneObj["address"]
    → 第二层调用结束,返回 { address: "北京" }
  
  // 回到第一层
  → 把第二层返回的 { address: "北京" } 赋值给第一层的 cloneObj["info"]
  → 第一层调用结束,返回完整的拷贝对象

2. cloneObj 不冲突的关键

 
  • cloneObj 是在 deepCloneBasic 函数内部用 let 声明的局部变量,每次调用函数时,都会重新创建一个全新的 cloneObj,和其他调用的 cloneObj 内存地址完全不同。
  • 每一层递归的 cloneObj 只服务于当前层要拷贝的对象 / 数组:
    • 第一层的 cloneObj 负责装 obj1 的所有属性(name、age、hobbies、info);
    • 第二层的 cloneObj 只负责装 obj1.info 的属性(address);
    • 处理数组 hobbies 时,第三层的 cloneObj 是数组 [],只装 ["篮球", "游戏"],和前两层的对象型 cloneObj 也完全独立。
    •  

3. 直观举例:变量的独立性

 
可以用简单代码验证局部变量的独立性:
function test(num) {
  let local = num; // 每次调用都会新建 local
  console.log("当前 local:", local);
  if (num > 0) {
    test(num - 1); // 递归调用
  }
  console.log("调用结束,local 仍为:", local);
}

test(2);
// 输出:
// 当前 local:2
// 当前 local:1
// 当前 local:0
// 调用结束,local 仍为:0
// 调用结束,local 仍为:1
// 调用结束,local 仍为:2

可以看到,每次递归的 local 都是独立的,修改内层的 local 不会影响外层,你的 cloneObj 原理完全一致。

也就是说:

 
  1. cloneObj 是函数内的局部变量,每次递归调用函数都会创建一个全新的 cloneObj,内存地址独立,互不干扰;
  2. 每一层的 cloneObj 只负责存储当前层要拷贝的对象 / 数组的内容,递归结束后返回给上一层,最终组装成完整的深拷贝结果;
  3. 递归的 “分层处理”+ 局部变量的 “独立作用域”,是 cloneObj 不冲突的核心原因。

第二种深拷贝方法:

JSON.parse(JSON.stringify())(简单场景可用)

const obj1 = { a: 1, b: { c: 2 } };
const obj2 = JSON.parse(JSON.stringify(obj1));

obj2.b.c = 999;
console.log(obj1.b.c); // 2 → 不受影响
✅ 优点:简单、一行代码
❌ 缺点:
  • 无法处理函数、undefinedSymbol(会被忽略或转为 null
  • 不能处理循环引用(会报错)
  • 会丢失原型链、DateRegExp 等特殊对象的类型(变成普通对象或字符串)

第三种深拷贝方法:

使用第三方库(生产推荐)

    • Lodash: _.cloneDeep(obj)
    •   
const _ = require('lodash');
const obj2 = _.cloneDeep(obj1);

 

 

 

posted @ 2026-01-20 21:34  chenlight  阅读(0)  评论(0)    收藏  举报