面试官:说说深浅拷贝?我:从Object.assign()到递归深拷贝的奇幻之旅 🚀

74 阅读6分钟

面试官:说说深浅拷贝?我:从Object.assign()到递归深拷贝的奇幻之旅 🚀

面试官:请谈谈你对深浅拷贝的理解?
我:(眼睛一亮)那我们就从Object.assign()这个神奇的API开始说起吧!

一、Object.assign():浅拷贝的魔法棒 ✨

1.1 基础概念

Object.assign()是ES6提供的对象操作方法,用于将一个或多个源对象的可枚举属性复制到目标对象。它就像复印机一样,能快速获取源对象的属性:

const target = { a: 1 };
const source = { b: 2 };
const result = Object.assign(target, source);

console.log(result); // { a: 1, b: 2 }
console.log(result === target); // true

1.2 特性揭秘

这个复印机有些有趣的特性:

  • 后来居上:属性冲突时,后面的源对象会覆盖前面的
Object.assign({ a: 1 }, { a: 2, b: 3 }, { b: 4 });
// { a: 2, b: 4 } - 后来者居上!
  • 特殊值处理nullundefined会被忽略
Object.assign({}, null, undefined); // 安全通过,无事发生
  • 原型链限制:不会复制原型链上的属性
const parent = { inherited: 'from parent' };
const child = Object.create(parent);
child.own = 'mine';

Object.assign({}, child); // { own: 'mine' } - 只复制自己的财产

1.3 业务场景:参数合并神器

在实际开发中,Object.assign()是处理配置合并的利器:

function createUser(options) {
  const defaults = {
    name: '匿名用户',
    age: 18,
    isAdmin: false
  };
  
  // 用户配置覆盖默认配置
  return Object.assign({}, defaults, options);
}

const user = createUser({ name: '小明', age: 20 });
console.log(user); 
// { name: '小明', age: 20, isAdmin: false }

二、深浅拷贝的深渊 🌌

2.1 赋值 vs 浅拷贝 vs 深拷贝

方式特点示例
赋值共享内存地址const b = a
浅拷贝只复制第一层Object.assign(), slice()
深拷贝完全独立的内存副本JSON.parse(JSON.stringify())

2.2 浅拷贝的陷阱

当对象有嵌套结构时,浅拷贝就会露出獠牙:

如果源对象的属性值是引用类型,那么目标对象的属性值会指向源对象的属性值的内存地址,改变目标对象的属性值,会影响源对象的属性值,不安全

const source = {
  profile: { name: '小明' },
  hobbies: ['篮球', '足球']
};

const shallowCopy = Object.assign({}, source);

// 修改拷贝后的对象
shallowCopy.profile.name = '小红';
shallowCopy.hobbies.push('游泳');

console.log(source.profile.name); // '小红' - 源数据被污染!
console.log(source.hobbies);      // ['篮球', '足球', '游泳']

2.3 数组的浅拷贝方法

除了Object.assign(),数组也有自己的浅拷贝方式:

// 方法1:slice()
const arr1 = [1, 2, {a: 3}];
const copy1 = arr1.slice();

// 方法2:concat()
const copy2 = arr1.concat();

// 方法3:展开运算符
const copy3 = [...arr1];

concat() 用于合并数组,但不传参数时,相当于复制原数组并返回新数组。 slice() 不传参数时,等价于 slice(0),表示从头到尾复制,返回一个新数组。

slice()concat()... 都会返回一个全新的数组,是实现数组浅拷贝的常用且有效的方式。其中 展开运算符 ... 是现代 JavaScript 中最推荐的写法,语法清晰、可读性强。

三、深拷贝的圣杯 🏆

3.1 JSON大法 - 简单粗暴

最简单的深拷贝方法:

深拷贝原理是:通过序列化再反序列化,切断所有引用关系,从而生成一个完全独立的新对象。简单有效,但有局限,不能处理函数(不知道怎么序列化函数)、symbol也不会拷贝、undefined 会被忽略、循环引用等特殊值。

const source = {
  profile: { name: '小明' },
  hobbies: ['篮球', '足球']
};

const deepCopy = JSON.parse(JSON.stringify(source));

适用场景

  • 拷贝纯数据对象(只包含数组、对象、字符串、数字、布尔值)
  • 临时使用,追求简洁
  • 不涉及函数、日期、循环引用等复杂结构

3.2 JSON方法的局限性

但这种方法有三大致命缺陷:

  1. 函数丢失 - 函数不会被复制
  2. 特殊值消失 - undefinedSymbol类型会消失
  3. 循环引用崩溃 - 遇到循环引用会抛出错误
const problemObj = {
  func: () => console.log('hello'),
  undef: undefined,
  sym: Symbol('id'),
  // 循环引用
  self: null
};
problemObj.self = problemObj;

JSON.parse(JSON.stringify(problemObj));
// 报错: Converting circular structure to JSON

四、手写深拷贝:征服复杂对象 🛠️

4.1 基础版本实现

解决JSON方法的局限性,我们需要自己打造深拷贝工具:

function deepClone(source) {
  // 基本类型直接返回
  if (source === null || typeof source !== 'object') {
    return source;
  }
  
  // 处理数组
  if (Array.isArray(source)) {
    return source.map(item => deepClone(item));
  }
  
  // 处理普通对象
  const clone = {};
  for (const key in source) {
    if (source.hasOwnProperty(key)) {
      clone[key] = deepClone(source[key]);
    }
  }
  
  return clone;
}

hasOwnProperty

深拷贝时,我们通常 只关心对象“自己定义的属性” ,而不希望把继承来的属性也拷贝过来(比如 toStringconstructor 等)。否则可能会:

  • 拷贝冗余/无用的属性
  • 引发性能问题
  • 甚至导致无限递归(某些特殊对象) 所以用 hasOwnProperty 来过滤掉继承属性,只遍历“自有属性”。

4.2 高级版本:解决循环引用

基础版本遇到循环引用会栈溢出,我们需要使用WeakMap来记录已拷贝对象:

function deepClone(source, map = new WeakMap()) {
  // 基本类型直接返回
  if (source === null || typeof source !== 'object') {
    return source;
  }
  
  // 解决循环引用
  if (map.has(source)) {
    return map.get(source);
  }
  
  // 初始化克隆对象
  const clone = Array.isArray(source) ? [] : {};
  map.set(source, clone);
  
  // 递归拷贝
  for (const key in source) {
    if (source.hasOwnProperty(key)) {
      clone[key] = deepClone(source[key], map);
    }
  }
  
  return clone;
}

// 测试循环引用
const obj = { a: 1 };
obj.self = obj;
const clone = deepClone(obj);
console.log(clone.self === clone); // true - 完美解决!

4.3 终极版本:支持各种类型

完善各种特殊类型的处理:

function deepClone(source, map = new WeakMap()) {
  // 处理基本类型
  if (source === null || typeof source !== 'object') {
    return source;
  }
  
  // 处理循环引用
  if (map.has(source)) return map.get(source);
  
  // 处理特殊对象类型
  switch (true) {
    case source instanceof Date:
      return new Date(source);
      
    case source instanceof RegExp:
      return new RegExp(source);
      
    case Array.isArray(source):
      const arrClone = [];
      map.set(source, arrClone);
      source.forEach((item, i) => {
        arrClone[i] = deepClone(item, map);
      });
      return arrClone;
      
    case source instanceof Map:
      const mapClone = new Map();
      map.set(source, mapClone);
      source.forEach((value, key) => {
        mapClone.set(key, deepClone(value, map));
      });
      return mapClone;
      
    case source instanceof Set:
      const setClone = new Set();
      map.set(source, setClone);
      source.forEach(value => {
        setClone.add(deepClone(value, map));
      });
      return setClone;
      
    default:
      // 普通对象
      const objClone = Object.create(Object.getPrototypeOf(source));
      map.set(source, objClone);
      
      // 处理Symbol属性
      const symbols = Object.getOwnPropertySymbols(source);
      [...Object.keys(source), ...symbols].forEach(key => {
        objClone[key] = deepClone(source[key], map);
      });
      
      return objClone;
  }
}

五、面试技巧:如何惊艳面试官 💫

当面试官问到深浅拷贝时,可以这样展示:

  1. 从API切入:先展示Object.assign()的熟练使用

    "我在项目中常用Object.assign()处理配置合并..."
    
  2. 深入原理:解释浅拷贝的局限性

    "但要注意它只是浅拷贝,嵌套对象会共享引用..."
    
  3. 解决方案:展示深拷贝的各种方案

    "对于简单场景可以用JSON方法,但要注意它的局限性..."
    
  4. 终极武器:手写完整深拷贝

    "在复杂场景下,我通常会实现这样的深拷贝函数..."
    
  5. 性能考量:补充优化思路

    "对于超大对象,可以考虑使用循环代替递归避免栈溢出..."
    

六、总结:拷贝的艺术 🎨

拷贝方式使用场景注意事项
赋值操作简单数据类型传递共享内存地址
Object.assign()单层对象合并/拷贝嵌套对象仍是引用
JSON方法简单对象深拷贝丢失函数/Symbol/循环引用问题
手写深拷贝复杂对象/需要完整复制注意循环引用和特殊类型处理

💡 黄金法则:根据你的业务场景选择拷贝方式:

  • 简单数据?直接用=赋值
  • 单层对象?Object.assign()或展开运算符
  • 简单嵌套?JSON.parse(JSON.stringify())
  • 复杂对象?上终极深拷贝函数