面试官:请手写一个深浅拷贝函数?我:这题我会,还能给你唠出花来

59 阅读3分钟

一、拷贝界的「表面兄弟」与「灵魂伴侣」

深浅拷贝这对CP,就像奶茶里的珍珠和芋圆——看似相似,实则本质不同。

  • 浅拷贝:表面兄弟,只复制对象的「外壳」。就像你和朋友买了同款手机壳,看似一样,但手机里的聊天记录还是各自的。
  • 深拷贝:灵魂伴侣,连对象的「内心世界」都复制得一模一样。相当于克隆了一个完全相同的你,连你藏在备忘录里的暗恋对象都复制过去了。

二、V8引擎的「储物间」:栈内存与堆内存

要理解拷贝,得先知道JavaScript的数据都存哪儿。V8引擎有两个「储物间」:

  • 栈内存:像超市的小储物柜,存着基本数据类型(数字、字符串等)和引用数据类型的「地址牌」。
  • 堆内存:像大型仓库,存着引用数据类型的「真身」(对象、数组等)。

当你复制一个对象时,浅拷贝只复制了「地址牌」,深拷贝则是连「仓库里的东西」都复制了一份。

三、浅拷贝:6种「表面功夫」哪家强?

1. Object.create(obj):「认干爹」法

const obj = { name: '康少', age: 18 };
const obj2 = Object.create(obj); // obj2认obj当干爹
console.log(obj2.name); // '康少'(继承干爹的财产)

2. [].concat(arr):「黏合剂」法

const arr = [1, 2, 3];
const newArr = [].concat(arr); // 用空数组黏合原数组

3. 数组解构 [...arr]:「分身术」法

const arr = [1, 2, 3];
const newArr = [...arr]; // 打散原数组,再组装成新数组

4. arr.slice(0, arr.length):「一刀切」法

const arr = ['a', 'b', 'c'];
const newArr = arr.slice(0, arr.length); // 从开头切到结尾,得到一个新数组

5. Object.assign({}, obj):「资产重组」法

const obj = { name: '康少', age: 18 };
const newObj = Object.assign({}, obj); // 把obj的财产转移到新对象

6. arr.toReversed().reverse():「折腾主义」法

const arr = [1, 2, 3];
const newArr = arr.toReversed().reverse(); // 先反转,再反转回来,得到新数组

注意:以上方法都只能拷贝对象的最外层。如果对象里还有对象(嵌套对象),修改原对象的内层属性,新对象也会跟着变。就像你复制了一份文件夹,里面的子文件夹还是指向原来的位置。

四、深拷贝:「灵魂复制」的三种姿势

1. JSON.parse(JSON.stringify(obj)):「JSON大法」

const obj = { name: '康少', age: 18, like: { a: '唱', b: '跳' } };
const newObj = JSON.parse(JSON.stringify(obj)); // 先转成JSON字符串,再转回对象

但这招有三个致命缺点:

  • 无法识别bigint类型
  • 无法处理undefined、symbol、function
  • 遇到循环引用直接「罢工」

2. structuredClone():「官方认证」法

const obj = { name: '康少', age: 18, like: { a: '唱', b: '跳' } };
const newObj = structuredClone(obj); // 浏览器自带的深拷贝API

这招比JSON大法强,能处理循环引用,但仍不支持function。

3. 手写深拷贝:「手动搓一个灵魂」

function deepCopy(obj) {
  // 处理null和非对象类型
  if (obj === null || typeof obj !== 'object') return obj;

  // 处理数组
  if (Array.isArray(obj)) {
    const newArr = [];
    for (let i = 0; i < obj.length; i++) {
      newArr[i] = deepCopy(obj[i]);
    }
    return newArr;
  }

  // 处理对象
  const newObj = {};
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      newObj[key] = deepCopy(obj[key]);
    }
  }
  return newObj;
}

这是终极解决方案,但还可以优化:

  • 处理Date、RegExp等特殊对象
  • 使用Map缓存已拷贝对象,避免循环引用

五、总结:拷贝界的「择偶标准」

  • 如果你只是想复制一个简单对象,用浅拷贝就够了(推荐Object.assign或解构赋值)。
  • 如果你要复制的对象里有嵌套对象,且没有特殊类型,用structuredClone
  • 如果你要处理复杂对象或循环引用,就得手写深拷贝了。

记住:深浅拷贝的核心区别,在于是否拷贝了引用类型的「真身」。理解了这一点,面试时再被问到,你就能侃侃而谈了!

(偷偷说一句:如果面试官问你怎么处理循环引用,你就把上面的深拷贝函数拿出来,再加上Map缓存,保证他对你刮目相看!)