【手撕系列】手撕深浅拷贝!

594 阅读2分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路

1. 浅拷贝

分为对象浅拷贝和数组浅拷贝 都是遍历它们的key然后直接赋值给新对象的相应key,由于是直接赋值,因此对于引用数据类型赋值的知识引用地址,所以是浅拷贝

/**
 * 浅拷贝对象
 * @param {Object} obj 对象
 */
function shallowClone(obj) {
  if (typeof obj !== 'object') return;

  // 判断是数组对象还是普通对象
  const copiedObj = Array.isArray(obj) ? [] : {};

  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      // 只拷贝对象自身的可枚举属性 不拷贝其隐式原型及原型链上的属性
      copiedObj[key] = obj[key];
    }
  }

  return copiedObj;
}

测试

先测试一下浅拷贝一个普通对象,我们希望拷贝一个对象foo,它里面包含了各种类型的属性

const foo = {
  name: 'plasticine',
  age: 20,
  isMale: true,
  friends: ['Alan', 'Jack', 'Mike'],
  info: {
    hobby: 'coding',
    height: '178cm',
  },
  date: new Date(),
  regExp: /^hello$/,
  say() {
    console.log('hi');
  },
};

然后调用shallowClone得到浅拷贝后的对象copiedFoo

const copiedFoo = shallowClone(foo);

接着我们修改一下原对象中的每个属性,看看是否会对copiedFoo对象有影响

// 修改基础数据类型
foo.name = 'foo';
foo.age = 21;

// 修改引用数据类型
foo.friends.push('Steven');
foo.info.weight = '80kg';

// 修改 js 内置对象类型
foo.date.newProp = 'date new prop';
foo.regExp.newProp = 'regExp new prop';

// 修改函数类型
foo.say.hello = 'hello';

先看看原来的foo对象

{
  name: 'foo',
  age: 21,
  isMale: true,
  friends: [ 'Alan', 'Jack', 'Mike', 'Steven' ],
  info: { hobby: 'coding', height: '178cm', weight: '80kg' },
  date: 2022-05-13T03:00:45.686Z { newProp: 'date new prop' },
  regExp: /^hello$/ { newProp: 'regExp new prop' },
  say: [Function: say] { hello: 'hello' }
}

再看看copiedFoo对象

{
  name: 'plasticine',
  age: 20,
  isMale: true,
  friends: [ 'Alan', 'Jack', 'Mike', 'Steven' ],
  info: { hobby: 'coding', height: '178cm', weight: '80kg' },
  date: 2022-05-13T03:00:45.686Z { newProp: 'date new prop' },
  regExp: /^hello$/ { newProp: 'regExp new prop' },
  say: [Function: say] { hello: 'hello' }
}

符合浅拷贝的特点,只有基础数据类型拷贝下来了,而引用数据类型的属性都是原对象foo的引用 再看看浅拷贝数组

const arr = [1, 2, true, '1', '2', { name: 'plasticine' }];
const copiedArr = shallowClone(arr);

修改一下数组中的元素

// 修改基础数据类型
arr[0] = 666;
arr[1] = 777;
arr[2] = false;
arr[3] = '666';
arr[4] = '777';
// 修改引用数据类型
arr[5].name = 'arr';

看看结果

console.log(arr); // [ 666, 777, false, '666', '777', { name: 'arr' } ]
console.log(copiedArr); // [ 1, 2, true, '1', '2', { name: 'arr' } ]

2. 深拷贝

2.1 简易版

首先实现一个简易版的,不考虑函数类型以及js内置的对象,只拷贝普通对象,只需要将前面浅拷贝的直接赋值的代码改成递归调用深拷贝函数即可

/**
 * @description 简易版深拷贝 -- 不考虑 js 内置对象和函数 以及 循环引用
 * @param {Object} obj 对象
 */
function deepCloneSimple(obj) {
  if (typeof obj !== 'object') return obj;

  const copiedObj = Array.isArray(obj) ? [] : {};

  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      copiedObj[key] = deepCloneSimple(obj[key]);
    }
  }

  return copiedObj;
}

测试:还是使用浅拷贝中的测试用例

const foo = {
  name: 'plasticine',
  age: 20,
  isMale: true,
  friends: ['Alan', 'Jack', 'Mike'],
  info: {
    hobby: 'coding',
    height: '178cm',
  },
  date: new Date(),
  regExp: /^hello$/,
  say() {
    console.log('hi');
  },
};

const copiedFoo = deepCloneSimple(foo);
// 修改基础数据类型
foo.name = 'foo';
foo.age = 21;

// 修改引用数据类型
foo.friends.push('Steven');
foo.info.weight = '80kg';

// 修改 js 内置对象类型
foo.date.newProp = 'date new prop';
foo.regExp.newProp = 'regExp new prop';

// 修改函数类型
foo.say.hello = 'hello';

先看看原对象foo

{
  name: 'foo',
  age: 21,
  isMale: true,
  friends: [ 'Alan', 'Jack', 'Mike', 'Steven' ],
  info: { hobby: 'coding', height: '178cm', weight: '80kg' },
  date: 2022-05-13T03:06:05.109Z { newProp: 'date new prop' },
  regExp: /^hello$/ { newProp: 'regExp new prop' },
  say: [Function: say] { hello: 'hello' }
}

再看看深拷贝对象copiedFoo

{
  name: 'plasticine',
  age: 20,
  isMale: true,
  friends: [ 'Alan', 'Jack', 'Mike' ],
  info: { hobby: 'coding', height: '178cm' },
  date: {},
  regExp: {},
  say: [Function: say] { hello: 'hello' }
}

普通对象能够成功深拷贝,而DateRegExp对象拷贝失败,居然变成了一个空对象

这是因为DateRegExp对象无法遍历得到它们的属性,而我们目前的实现中是通过遍历对象所有的key去进行拷贝的,所以会导致最终得到的是一个空对象的情况

而函数类型目前是和基础数据类型一样的待遇,因此是直接拷贝引用,所以仍然是浅拷贝


2.2 完全版

对于函数类型和js内置的对象类型,比如DateRegExp对象,由于我们无法遍历这些对象的key,因此不能直接递归调用深拷贝函数去进行拷贝,而应该调用它们的构造函数去进行拷贝,这就意味着我们需要作出额外的判断去处理

还需要考虑MapSet对象的拷贝,如果和处理DateRegExp一样直接调用它们的构造函数的话,得到的还是浅拷贝,因此我们应当遍历MapSet去一一深拷贝

此外,还要考虑到循环引用的问题,因此我们可以用一个类似缓存的机制,如果缓存中有已经拷贝过的key时,直接返回,不进行拷贝,为了不让垃圾回收机制无法回收缓存中的对象,我们需要使用WeakMap而不是Map

/**
 * @description 深拷贝
 * @param {Object} obj 对象
 * @param {WeakMap} circleMap 用于标记属性是否被循环引用
 */
function deepClone(obj, circleMap = new WeakMap()) {
  if (typeof obj !== 'object') {
    // 基础数据类型和函数类型无需
    return obj;
  }

  // 循环引用直接返回
  if (circleMap.get(obj) === true) return obj;

  // 深拷贝 Map 对象
  if (obj instanceof Map) {
    const copiedMap = new Map();
    obj.forEach((value, key) => copiedMap.set(key, deepClone(value)));

    // 存入缓存
    circleMap.set(obj, true);

    return copiedMap;
  }

  // 深拷贝 Set 对象
  if (obj instanceof Set) {
    const copiedSet = new Set();
    obj.forEach((value) => copiedSet.add(value));

    // 存入缓存
    circleMap.set(obj, true);

    return copiedSet;
  }

  // 深拷贝 Date 和 RegExp 类型
  const constructor = obj.constructor;
  if (/^(RegExp|Date)$/i.test(constructor.name)) {
    // RegExp 或 Date 类型

    // 存入缓存
    circleMap.set(obj, true);

    return new constructor(obj);
  }

  // 处理 object 和 function 类型
  if (typeof obj === 'object' && obj !== null) {
    // 标记当前遍历的 key 已经被拷贝过
    circleMap.set(obj, true);

    // 深拷贝对象或数组
    const copiedObj = Array.isArray(obj) ? [] : {};
    for (let key in obj) {
      if (obj.hasOwnProperty(key)) {
        copiedObj[key] = deepClone(obj[key], circleMap);
      }
    }

    return copiedObj;
  }
}

测试:深拷贝foo对象

const foo = {
  name: 'plasticine',
  age: 20,
  isMale: true,
  friends: ['Alan', 'Jack', 'Mike'],
  info: {
    hobby: 'coding',
    height: '178cm',
  },
  date: new Date(),
  regExp: /^hello$/,
  say() {
    console.log('hi');
  },
  map: new Map([
    ['key1', 'value1'],
    ['key2', 'value2'],
  ]),
  set: new Set([1, 1, 2, 3, 5, 8, 13, 1, 1, 2, 3, 5, 8, 13]),
};

// 添加循环引用
foo.circle = foo;

const copiedFoo = deepClone(foo);

对原对象作出一些修改

// 修改基础数据类型
foo.name = 'foo';
foo.age = 21;

// 修改引用数据类型
foo.friends.push('Steven');
foo.info.weight = '80kg';

// 修改 js 内置对象类型
foo.date.newProp = 'date new prop';
foo.regExp.newProp = 'regExp new prop';

// 修改函数类型
foo.say.hello = 'hello';

// 修改 Map 对象
foo.map.set('key1', 'hello');

// 修改 Set 对象
foo.set.delete(1);

原对象foo

<ref *1> {
  name: 'foo',
  age: 21,
  isMale: true,
  friends: [ 'Alan', 'Jack', 'Mike', 'Steven' ],
  info: { hobby: 'coding', height: '178cm', weight: '80kg' },
  date: 2022-05-13T03:42:34.617Z { newProp: 'date new prop' },
  regExp: /^hello$/ { newProp: 'regExp new prop' },
  say: [Function: say] { hello: 'hello' },
  map: Map(2) { 'key1' => 'hello', 'key2' => 'value2' },
  set: Set(5) { 2, 3, 5, 8, 13 },
  circle: [Circular *1]
}

新对象

{
  name: 'plasticine',
  age: 20,
  isMale: true,
  friends: [ 'Alan', 'Jack', 'Mike' ],
  info: { hobby: 'coding', height: '178cm' },
  date: 2022-05-13T03:42:34.617Z,
  regExp: /^hello$/,
  say: [Function: say] { hello: 'hello' },
  map: Map(2) { 'key1' => 'value1', 'key2' => 'value2' },
  set: Set(6) { 1, 2, 3, 5, 8, 13 },
  circle: <ref *1> {
    name: 'foo',
    age: 21,
    isMale: true,
    friends: [ 'Alan', 'Jack', 'Mike', 'Steven' ],
    info: { hobby: 'coding', height: '178cm', weight: '80kg' },
    date: 2022-05-13T03:42:34.617Z { newProp: 'date new prop' },
    regExp: /^hello$/ { newProp: 'regExp new prop' },
    say: [Function: say] { hello: 'hello' },
    map: Map(2) { 'key1' => 'hello', 'key2' => 'value2' },
    set: Set(5) { 2, 3, 5, 8, 13 },
    circle: [Circular *1]
  }
}