返回首页

20. 说说你对深拷贝和浅拷贝的理解?

问题解析

深拷贝和浅拷贝是JavaScript中处理对象复制的核心概念。理解它们的区别、实现方式以及应用场景,对于避免程序中的意外副作用和数据污染至关重要。

核心概念

数据类型存储

JavaScript中存在两大数据类型:

  • 基本类型:Number、String、Boolean、Undefined、Null、Symbol,存储在栈中
  • 引用类型:Object、Array、Function等,对象存储在堆中,引用存储在栈中

浅拷贝(Shallow Copy)

浅拷贝,指的是创建新的数据,这个数据有着原始数据属性值的一份精确拷贝。

特点

  • 如果属性是基本类型,拷贝的就是基本类型的值
  • 如果属性是引用类型,拷贝的就是内存地址
  • 即浅拷贝是拷贝一层,深层次的引用类型则共享内存地址

深拷贝(Deep Copy)

深拷贝开辟一个新的栈,两个对象属性完全相同,但是对应两个不同的地址,修改一个对象的属性,不会改变另一个对象的属性。

特点

  • 递归拷贝对象的所有层级
  • 新旧对象完全独立,互不影响
  • 会创建新的内存空间存储所有数据

详细解答

浅拷贝的实现方式

1. Object.assign

const obj = {
  age: 18,
  nature: ['smart', 'good'],
  names: {
    name1: 'fx',
    name2: 'xka'
  },
  love: function () {
    console.log('fx is a great girl');
  }
};

const newObj = Object.assign({}, obj);

// 测试
newObj.age = 20;
console.log(obj.age); // 18,基本类型互不影响

newObj.names.name1 = 'new name';
console.log(obj.names.name1); // 'new name',引用类型共享内存

2. Array.prototype.slice()

const fxArr = ["One", "Two", "Three"];
const fxArrs = fxArr.slice(0);

fxArrs[1] = "love";
console.log(fxArr); // ["One", "Two", "Three"]
console.log(fxArrs); // ["One", "love", "Three"]

// 但对于嵌套数组
const arr = [[1, 2], [3, 4]];
const arrCopy = arr.slice();
arrCopy[0][0] = 'changed';
console.log(arr[0][0]); // 'changed',嵌套对象仍是浅拷贝

3. Array.prototype.concat()

const fxArr = ["One", "Two", "Three"];
const fxArrs = fxArr.concat();

fxArrs[1] = "love";
console.log(fxArr); // ["One", "Two", "Three"]

4. 拓展运算符

const fxArr = ["One", "Two", "Three"];
const fxArrs = [...fxArr];

fxArrs[1] = "love";
console.log(fxArr); // ["One", "Two", "Three"]

// 对象
const obj = { a: 1, b: { c: 2 } };
const objCopy = { ...obj };
objCopy.b.c = 3;
console.log(obj.b.c); // 3,嵌套对象仍是浅拷贝

深拷贝的实现方式

1. JSON.parse(JSON.stringify())

const obj2 = JSON.parse(JSON.stringify(obj1));

// 优点:简单快捷
// 缺点:
// 1. 会忽略 undefined
// 2. 会忽略 symbol
// 3. 会忽略函数
// 4. 不能处理循环引用
// 5. 不能处理 Date、RegExp、Map、Set等特殊对象

const obj = {
  name: 'A',
  name1: undefined,
  name3: function() {},
  name4: Symbol('A')
};
const obj2 = JSON.parse(JSON.stringify(obj));
console.log(obj2); // {name: "A"}

2. Lodash的cloneDeep

const _ = require('lodash');

const obj1 = {
  a: 1,
  b: { f: { g: 1 } },
  c: [1, 2, 3]
};

const obj2 = _.cloneDeep(obj1);
console.log(obj1.b.f === obj2.b.f); // false

3. jQuery.extend()

const $ = require('jquery');

const obj1 = {
  a: 1,
  b: { f: { g: 1 } },
  c: [1, 2, 3]
};

const obj2 = $.extend(true, {}, obj1);
console.log(obj1.b.f === obj2.b.f); // false

4. 手写递归实现

function deepClone(obj, hash = new WeakMap()) {
  // 如果是null或者undefined,不进行拷贝操作
  if (obj === null) return obj;

  // 处理Date
  if (obj instanceof Date) return new Date(obj);

  // 处理RegExp
  if (obj instanceof RegExp) return new RegExp(obj);

  // 处理基本类型
  if (typeof obj !== "object") return obj;

  // 处理循环引用
  if (hash.get(obj)) return hash.get(obj);

  // 找到的是所属类原型上的constructor,指向当前类本身
  let cloneObj = new obj.constructor();

  hash.set(obj, cloneObj);

  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      // 实现一个递归拷贝
      cloneObj[key] = deepClone(obj[key], hash);
    }
  }

  return cloneObj;
}

完整的深拷贝实现

function deepClone(obj, hash = new WeakMap()) {
  // 处理null
  if (obj === null) return null;

  // 处理基本类型
  if (typeof obj !== 'object') return obj;

  // 处理Date
  if (obj instanceof Date) return new Date(obj);

  // 处理RegExp
  if (obj instanceof RegExp) return new RegExp(obj);

  // 处理Map
  if (obj instanceof Map) {
    const mapCopy = new Map();
    obj.forEach((value, key) => {
      mapCopy.set(deepClone(key, hash), deepClone(value, hash));
    });
    return mapCopy;
  }

  // 处理Set
  if (obj instanceof Set) {
    const setCopy = new Set();
    obj.forEach(value => {
      setCopy.add(deepClone(value, hash));
    });
    return setCopy;
  }

  // 处理循环引用
  if (hash.has(obj)) return hash.get(obj);

  // 处理数组
  if (Array.isArray(obj)) {
    const arrCopy = [];
    hash.set(obj, arrCopy);
    obj.forEach((item, index) => {
      arrCopy[index] = deepClone(item, hash);
    });
    return arrCopy;
  }

  // 处理普通对象
  const objCopy = {};
  hash.set(obj, objCopy);

  Object.keys(obj).forEach(key => {
    objCopy[key] = deepClone(obj[key], hash);
  });

  // 拷贝Symbol类型的key
  Object.getOwnPropertySymbols(obj).forEach(sym => {
    objCopy[sym] = deepClone(obj[sym], hash);
  });

  return objCopy;
}

深入理解

浅拷贝与深拷贝的区别

// 浅拷贝示例
const obj1 = {
  name: 'init',
  arr: [1, [2, 3], 4],
};

const obj3 = shallowClone(obj1);
obj3.name = "update";
obj3.arr[1] = [5, 6, 7];

console.log('obj1', obj1);
// obj1 { name: 'init', arr: [ 1, [ 5, 6, 7 ], 4 ] }
console.log('obj3', obj3);
// obj3 { name: 'update', arr: [ 1, [ 5, 6, 7 ], 4 ] }
// 注意:obj1.arr也被修改了

// 深拷贝示例
const obj4 = deepClone(obj1);
obj4.name = "update";
obj4.arr[1] = [5, 6, 7];

console.log('obj1', obj1);
// obj1 { name: 'init', arr: [ 1, [ 2, 3 ], 4 ] }
console.log('obj4', obj4);
// obj4 { name: 'update', arr: [ 1, [ 5, 6, 7 ], 4 ] }
// obj1保持不变

内存示意图

浅拷贝:
栈内存              堆内存
obj1  ---------->  {name: 'init', arr: [1, [2,3], 4]}
                      ^
obj3  --------------|

深拷贝:
栈内存              堆内存
obj1  ---------->  {name: 'init', arr: [1, [2,3], 4]}
obj3  ---------->  {name: 'init', arr: [1, [2,3], 4]} (新的内存空间)

最佳实践

1. 根据场景选择

// 只有一层基本类型,浅拷贝即可
const simpleObj = { a: 1, b: 2 };
const copy = { ...simpleObj };

// 有多层嵌套,需要深拷贝
const nestedObj = { a: { b: { c: 1 } } };
const deepCopy = _.cloneDeep(nestedObj);

2. 使用结构化克隆(现代浏览器)

// structuredClone是浏览器内置的深拷贝方法
const original = {
  date: new Date(),
  map: new Map([[1, 'one']]),
  nested: { a: 1 }
};

const clone = structuredClone(original);

// 限制:
// 1. 不能克隆函数
// 2. 不能克隆DOM节点
// 3. 不能克隆某些特殊对象

3. 性能考虑

// 对于大对象,深拷贝可能很耗时
// 考虑使用不可变数据模式

// Immutable.js
import { Map } from 'immutable';

const map1 = Map({ a: 1, b: 2, c: 3 });
const map2 = map1.set('b', 50);
// map1保持不变,map2是新的对象,但共享未修改的部分

面试要点

  1. 本质区别:浅拷贝只复制一层,深拷贝递归复制所有层级
  2. 实现方式:能够列举多种浅拷贝和深拷贝的实现方法
  3. JSON方法的局限:了解JSON.parse(JSON.stringify())的缺陷
  4. 循环引用:手写深拷贝时需要处理循环引用
  5. 特殊对象:Date、RegExp、Map、Set等特殊对象的处理

常见问题

Q:浅拷贝和深拷贝有什么区别? A:浅拷贝只复制对象的第一层属性,如果属性是引用类型,拷贝的是内存地址;深拷贝递归复制所有层级的属性,创建完全独立的新对象。

Q:JSON.parse(JSON.stringify())有什么缺点? A:会忽略undefined、symbol和函数;不能处理循环引用;不能正确处理Date、RegExp、Map、Set等特殊对象。

Q:如何实现一个完美的深拷贝? A:需要递归处理所有数据类型,包括基本类型、对象、数组、Date、RegExp、Map、Set等,同时使用WeakMap处理循环引用。