一步一步手写深拷贝函数

111 阅读10分钟

一、前言

我们知道对象相互赋值的一些关系,分别包括:

  1. 引入的赋值:指向同一个对象,相互之间会影响;
  2. 对象的浅拷贝:只是浅层的拷贝,内部引入对象时,依然会相互影响;
  3. 对象的深拷贝:两个对象不再有任何关系,不会相互影响;

1.引用赋值:

info引用obj对象,info和obj指向同一个对象地址,info改变name,obj也跟着改变

const obj = {
  name: "why",
  friend: {
    name: "kobe"
  },
  foo: function() {
    console.log("foo function")
  },
  [s1]: "abc",
  s2: s2
}

const info = obj;
info.name = 'info'
console.log('info, obj>>>>>', info, obj);

打印结果如下:(name都变成了info)

info, obj>>>>> {
  name: 'info',
  friend: { name: 'kobe' },
  foo: [Function: foo],
  s2: Symbol(),
  [Symbol()]: 'abc'
} {
  name: 'info',
  friend: { name: 'kobe' },
  foo: [Function: foo],
  s2: Symbol(),
  [Symbol()]: 'abc'
} 
#名字跟着变成了info

2.浅拷贝

info对obj进行了浅拷贝,现在info所指对象和obj所指对象有不一样的内存地址,但是他们所指对象中的复杂数据结构的内存地址还是同一个,所以info.friend.name改变也会影响到obj.friend.name 创建方法:三点拓展符号,object.assign()

const s1 = Symbol()
const s2 = Symbol()

const obj = {
  name: "why",
  friend: {
    name: "kobe"
  },
  foo: function() {
    console.log("foo function")
  },
  [s1]: "abc",
  s2: s2
}

const info = { ...obj };
info.name = 'info'
info.friend.name='coder66y'
console.log('info, obj>>>>>', info, obj);

打印结果如下:

info, obj>>>>> {
  name: 'info',
  friend: { name: 'coder66y' },
  foo: [Function: foo],
  s2: Symbol(),
  [Symbol()]: 'abc'
} {
  name: 'why',
  friend: { name: 'coder66y' },
  foo: [Function: foo],
  s2: Symbol(),
  [Symbol()]: 'abc'
}

3.深拷贝

info对obj进行了深拷贝,现在info所指对象和obj所指对象有不一样的内存地址,他们所指对象中的复杂数据结构的内存地址也是不一样的,所以info.friend.name改变不会影响到obj.friend.name 常见的深拷贝方法:JSON转换,lodash深拷贝方法

const s1 = Symbol()
const s2 = Symbol()

const obj = {
  name: "why",
  friend: {
    name: "kobe"
  },
  foo: function() {
    console.log("foo function")
  },
  [s1]: "abc",
  s2: s2
}

const info = JSON.parse(JSON.stringify(obj));
info.name = 'info'
info.friend.name='coder66y'
console.log('info, obj>>>>>', info, obj);

打印结果如下:

info, obj>>>>> { name: 'info', friend: { name: 'coder66y' } } {
  name: 'why',
  friend: { name: 'kobe' },
  foo: [Function: foo],
  s2: Symbol(),
  [Symbol()]: 'abc'
}

虽然以上的方法都可以深拷贝,但是难免有不足,比如json不能拷贝symbal数据,不能拷贝函数,循环引用问题没有解决,所以废话不多说,我们来实现一下手写深拷贝函数

二、手写深拷贝函数

实现以下功能:

  1. 自定义深拷贝的基本功能;
  2. 对Symbol的key进行处理;
  3. 其他数据类型的值进程处理:数组、函数、Symbol、Set、Map;
  4. 对循环引用的处理;

1. 基本实现(对基本数据类型进行拷贝,其实是浅拷贝)

function deepClone(originValue) {
  const newObject = {};
  for (const key in originValue) {
    newObject[key] = originValue[key];
  }
  return newObject;
};

// 测试代码
const obj = {
  name: 'coder66y',
  age: '18'
};

const newObj = deepClone(obj);
console.log('newObj === obj>>>>>', newObj === obj); // false 说明两者所指对象的内存不是同一个地址
console.log('newObj>>>>>', newObj); // { name: 'coder66y', age: '18' }
newObj.name = 'ly';
console.log('newObj, obj>>>>>', newObj, obj); // { name: 'ly', age: '18' } { name: 'coder66y', age: '18' }

这个时候,我们发现打印的确实现了拷贝,但是是深拷贝嘛?检查一下,并不是,现在只是一个浅拷贝

// 测试代码
const obj = {
  name: 'coder66y',
  age: '18',
  friend: {
    name: 'lss',
    age: '21'
  }
};

const newObj = deepClone(obj);
console.log('newObj === obj>>>>>', newObj === obj); // false 说明两者所指对象的内存不是同一个地址
console.log('newObj>>>>>', newObj); // { name: 'coder66y', age: '18' }
newObj.name = 'ly';
console.log('newObj, obj>>>>>', newObj, obj); // { name: 'ly', age: '18' } { name: 'coder66y', age: '18' }
newObj.friend.name = '熊大';
console.log('newObj, obj>>>>>', newObj, obj);
// { name: 'ly', age: '18', friend: { name: '熊大', age: '21' } } { name: 'coder66y', age: '18', friend: { name: '熊大', age: '21' } }

newObj.friend.name变化,obj.friend.name也跟着变化了,明显这是一个浅拷贝

2. 深拷贝(对其他类型进行深拷贝)

测试代码

// 测试代码
let s1 = Symbol();
let s2 = Symbol('s2');
const obj = {
  name: 'coder66y',
  age: '18',
  friend: {
    name: 'lss',
    age: '21',
    address: {
      city: '杭州',
    }
  },
  // Symbol作为key和value
  s1: s1,
  s2: s2,
  [s1]: 's1',
  [s2]: 's2',
  // 数组类型
  hobbies: ['唱歌', '跳舞', '画画'],
  // 函数类型
  hobby() {
    console.log('写代码');
  },
  set: new Set(['aaa', 'bbb']),
  map: new Map([['1', '111'], ['3', '333']]),
};

最常见的对对象深拷贝

function isObject(originValue) {
  if(['object', 'function'].includes(typeof originValue)) return true;
  return false;
}

function deepClone(originValue) {
  if (!isObject(originValue)) return originValue; // 不是对象直接返回

   // 判断传入的对象是数组, 还是对象
  const newObj = Array.isArray(originValue) ? []: {}
  for (const key in originValue) {
    newObj[key] = deepClone(originValue[key])
  }
  return newObj;
}

const newObj = deepClone(obj);
console.log('newObj === obj>>>>>', newObj === obj); // false 说明两者所指对象的内存不是同一个地址
newObj.name = 'ly'; // 没有影响到原来的obj
newObj.friend.name = '熊大'; // 没有影响到原来的obj
newObj.hobbies = ['buxihuan'] // 没有影响到原来的obj
console.log('newObj>>>>>', newObj, obj);

测试结果如下:

newObj === obj>>>>> false
newObj>>>>> {
name: 'ly',
age: '18',
friend: { name: '熊大', age: '21', address: { city: '杭州' } },
s1: Symbol(s1),
hobbies: [ 'buxihuan' ],
hobby: {},
set: {},
map: {}
} {
name: 'coder66y',
age: '18',
friend: { name: 'lss', age: '21', address: { city: '杭州' } },
s1: Symbol(s1),
hobbies: [ '唱歌', '跳舞', '画画' ],
hobby: [Function: hobby],
set: Set(2) { 'aaa', 'bbb' },
map: Map(2) { '1' => '111', '3' => '333' },
[Symbol(s1)]: 's1',
[Symbol(s2)]: 's2'
}

发现此时的对象数组都实现了深拷贝,newObj的更改,没有影响到原来的obj 但是我们发现函数并没有拷贝过来, 接着下一步:

实现函数的深拷贝

如果函数有返回值,由于函数本身内存性质的不同,其返回的值会重新占有内存,就不存在相互影响的问题;所以函数的深拷贝可以直接复用原来的函数,新的对象中的方法,指向该函数即可;

function isObject(originValue) {
  if(['object', 'function'].includes(typeof originValue)) return true;
  return false;
}

function deepClone(originValue) {
  // 新增
  if(typeof originValue === 'function') return originValue; // 是函数直接返回

  if (!isObject(originValue)) return originValue; // 不是对象直接返回

  // 判断传入的对象是数组, 还是对象
  const newObj = Array.isArray(originValue) ? []: {}
  for (const key in originValue) {
    newObj[key] = deepClone(originValue[key])
  }
  return newObj;
}

const newObj = deepClone(obj);
console.log('newObj === obj>>>>>', newObj === obj); // false 说明两者所指对象的内存不是同一个地址
console.log('newObj>>>>>', newObj, obj);

打印结果:

newObj === obj>>>>> false
newObj>>>>> {
name: 'coder66y',
age: '18',
friend: { name: 'lss', age: '21', address: { city: '杭州' } },
s1: Symbol(s1),
hobbies: [ '唱歌', '跳舞', '画画' ],
hobby: [Function: hobby],
set: {},
map: {}
} {
name: 'coder66y',
age: '18',
friend: { name: 'lss', age: '21', address: { city: '杭州' } },
s1: Symbol(s1),
hobbies: [ '唱歌', '跳舞', '画画' ],
hobby: [Function: hobby],
set: Set(2) { 'aaa', 'bbb' },
map: Map(2) { '1' => '111', '3' => '333' },
[Symbol(s1)]: 's1',
[Symbol(s2)]: 's2'
}

此时,函数实现了深拷贝, newObj.hobbyobj.hooby一样, 实现复用

symbol类型

由上面的打印可以看到Symbol对象作为key的时候,是可以被“拷贝”过来的,但是打印newObj.s1 === obj.s1会发现改值为true,意思是他们两的s1指的是同一个,那么会出现相互影响的问题,现在来完善:

function isObject(originValue) {
  if(['object', 'function'].includes(typeof originValue)) return true;
  return false;
}

function deepClone(originValue) {
  // 新增
  if (typeof originValue === 'symbol') return Symbol(originValue.description); // 返回新建的Symbol类型
  
  if (typeof originValue === 'function') return originValue; // 是函数直接返回

  if (!isObject(originValue)) return originValue; // 不是对象直接返回

  // 判断传入的对象是数组, 还是对象
  const newObj = Array.isArray(originValue) ? []: {}
  for (key in originValue) {
    newObj[key] = deepClone(originValue[key])
  }
  return newObj;
}

const newObj = deepClone(obj);
console.log('newObj === obj>>>>>', newObj === obj); // false 说明两者所指对象的内存不是同一个地址
console.log('newObj.s1 === obj.s1>>>>>', newObj.s1 === obj.s1); // false

此时newObj.s1 === obj.s1false;打印结果如下:

newObj === obj>>>>> false
newObj.s1 === obj.s1>>>>> false
newObj>>>>> {
  name: 'coder66y',
  age: '18',
  friend: { name: 'lss', age: '21', address: { city: '杭州' } },
  s1: Symbol(),
  s2: Symbol(s2),
  hobbies: [ '唱歌', '跳舞', '画画' ],
  hobby: [Function: hobby],
  set: {},
  map: {}
} {
  name: 'coder66y',
  age: '18',
  friend: { name: 'lss', age: '21', address: { city: '杭州' } },
  s1: Symbol(),
  s2: Symbol(s2),
  hobbies: [ '唱歌', '跳舞', '画画' ],
  hobby: [Function: hobby],
  set: Set(2) { 'aaa', 'bbb' },
  map: Map(2) { '1' => '111', '3' => '333' },
  [Symbol()]: 's1',
  [Symbol(s2)]: 's2'
}

发现symbol作为key的时候,并没有被复制,所以需要完善: 因为symbol类型使用for... of...的方法不能遍历,可以用Object.getOwnPropertySymbols()获取所有的symbol类型的key,然后遍历,给新的对象赋值;

function isObject(originValue) {
  if(['object', 'function'].includes(typeof originValue)) return true;
  return false;
}

function deepClone(originValue) {
  // key是symbol类型(新增)
  const symbalArr = Object.getOwnPropertySymbols(originValue);
  symbalArr.forEach(item => {
    newObj[item] = deepClone(originValue[item])
  })
  // value是Symbol类型
  if (typeof originValue === 'symbol') return Symbol(originValue.description); // 返回新建的symbol类型
  // value是函数类型
  if(typeof originValue === 'function') return originValue; // 是函数直接返回

  if (!isObject(originValue)) return originValue; // 不是对象直接返回

    // 判断传入的对象是数组, 还是对象
  const newObj = Array.isArray(originValue) ? []: {}
  for (const key in originValue) {
    newObj[key] = deepClone(originValue[key])
  }
  return newObj;
}


const newObj = deepClone(obj);
newObj[s1] = 's2'
console.log('obj[s1], newObj[s1]>>>>>', obj[s1], newObj[s1]); // s1, s2

console.log('newObj>>>>>', newObj, obj);

打印结果如下:

obj[s1], newObj[s1]>>>>> s1 s2
newObj>>>>> {
  name: 'coder66y',
  age: '18',
  friend: { name: 'lss', age: '21', address: { city: '杭州' } },
  s1: Symbol(),
  s2: Symbol(s2),
  hobbies: [ '唱歌', '跳舞', '画画' ],
  hobby: [Function: hobby],
  set: {},
  map: {},
  [Symbol()]: 's2',
  [Symbol(s2)]: 's2'
} {
  name: 'coder66y',
  age: '18',
  friend: { name: 'lss', age: '21', address: { city: '杭州' } },
  s1: Symbol(),
  s2: Symbol(s2),
  hobbies: [ '唱歌', '跳舞', '画画' ],
  hobby: [Function: hobby],
  set: Set(2) { 'aaa', 'bbb' },
  map: Map(2) { '1' => '111', '3' => '333' },
  [Symbol()]: 's1',
  [Symbol(s2)]: 's2'
}

现在不仅打印出了Symbol类型的key,而且也不会影响原有被拷贝的数据;

Set类型和Map类型:

set类型和map类型,此时不能用typeof 来区分setmap, 因为typeof new Set() = 'object', 变成instanceof 来进行类型判断

function isObject(originValue) {
  if(['object', 'function'].includes(typeof originValue)) return true;
  return false;
}

function deepClone(originValue) {

  // set类型,此时不能用typeof 来区分set, 因为typeof new Set() = 'object', 变成instanceof 来进行类型判断
  if ( originValue instanceof Set ) return new Set(originValue);
  if ( originValue instanceof Map ) return new Map(originValue);
  
  // value是Symbol类型
  if (typeof originValue === 'symbol') return Symbol(originValue.description); // 返回新建的symbol类型
  // value是函数类型
  if(typeof originValue === 'function') return originValue; // 是函数直接返回

  if (!isObject(originValue)) return originValue; // 不是对象直接返回

     // 判断传入的对象是数组, 还是对象
  const newObj = Array.isArray(originValue) ? []: {}
  for (key in originValue) {
    newObj[key] = deepClone(originValue[key])
  }

  // key是symbol类型
  const symbalArr = Object.getOwnPropertySymbols(originValue);
  symbalArr.forEach(item => {
    newObj[item] = deepClone(originValue[item])
  })
  return newObj;
}

const newObj = deepClone(obj);
newObj.set.add(1);
newObj.map.set(0, 'o');

console.log('newObj>>>>>', newObj, obj);

打印结果如下:

newObj>>>>> {
  name: 'coder66y',
  age: '18',
  friend: { name: 'lss', age: '21', address: { city: '杭州' } },
  s1: Symbol(),
  s2: Symbol(s2),
  hobbies: [ '唱歌', '跳舞', '画画' ],
  hobby: [Function: hobby],
  set: Set(3) { 'aaa', 'bbb', 1 },
  map: Map(3) { '1' => '111', '3' => '333', 0 => 'o' },
  [Symbol()]: 's1',
  [Symbol(s2)]: 's2'
} {
  name: 'coder66y',
  age: '18',
  friend: { name: 'lss', age: '21', address: { city: '杭州' } },
  s1: Symbol(),
  s2: Symbol(s2),
  hobbies: [ '唱歌', '跳舞', '画画' ],
  hobby: [Function: hobby],
  set: Set(2) { 'aaa', 'bbb' },
  map: Map(2) { '1' => '111', '3' => '333' },
  [Symbol()]: 's1',
  [Symbol(s2)]: 's2'
}

新的对象mapset值的改变,并没有影响到原来的对象, 深拷贝成功

针对类型的完整的深拷贝函数:

function isObject(originValue) {
  if(['object', 'function'].includes(typeof originValue)) return true;
  return false;
}

function deepClone(originValue) {
  // set类型,此时不能用typeof 来区分set, 因为typeof new Set() = 'object', 变成instanceof 来进行类型判断
  if ( originValue instanceof Set ) return new Set(originValue);
  if ( originValue instanceof Map ) return new Map(originValue);

  // value是Symbol类型
  if (typeof originValue === 'symbol') return Symbol(originValue.description); // 返回新建的symbol类型
  // value是函数类型
  if(typeof originValue === 'function') return originValue; // 是函数直接返回

  if (!isObject(originValue)) return originValue; // 不是对象直接返回


  // 判断传入的对象是数组, 还是对象
  const newObj = Array.isArray(originValue) ? []: {}
  for (key in originValue) {
    newObj[key] = deepClone(originValue[key])
  }

  // key是symbol类型
  const symbalArr = Object.getOwnPropertySymbols(originValue);
  symbalArr.forEach(item => {
    newObj[item] = deepClone(originValue[item])
  })
  return newObj;
}

3.实现循环引用的深拷贝

使用map数据结构,使用WeakMap() 为什么使用weakMap 而不是map? 因为弱引用有利于垃圾回收

function isObject(originValue) {
  if(['object', 'function'].includes(typeof originValue)) return true;
  return false;
}

function deepClone(originValue, map = new WeakMap()) {
  // value是set类型,此时不能用typeof 来区分set, 因为typeof new Set() = 'object', 变成instanceof 来进行类型判断
  if ( originValue instanceof Set ) return new Set(originValue);
  if ( originValue instanceof Map ) return new Map(originValue);

  // value是Symbol类型
  if (typeof originValue === 'symbol') return Symbol(originValue.description); // 返回新建的symbol类型

  // value是函数类型
  if(typeof originValue === 'function') return originValue; // 是函数直接返回

  if (!isObject(originValue)) return originValue; // 不是对象直接返回

  if (map.has(originValue)) {
    return map.get(originValue)
  }

  // 判断传入的对象是数组, 还是对象
  const newObj = Array.isArray(originValue) ? []: {}
  map.set(originValue, newObj)
  for (const key in originValue) {
    newObj[key] = deepClone(originValue[key], map)
  }

  // key是symbol类型
  const symbalArr = Object.getOwnPropertySymbols(originValue);
  symbalArr.forEach(item => {
    newObj[item] = deepClone(originValue[item])
  })
  return newObj;
}

obj.info = obj;

const newObj = deepClone(obj);
newObj.info.info = {0: 'o'}

console.log(newObj.info, obj.info);

打印如下:已经解决循环引用问题,newObjinfo改变没有影响到原来的obj

{ '0': 'o' } <ref *1> {
  name: 'coder66y',
  age: '18',
  friend: { name: 'lss', age: '21', address: { city: '杭州' } },
  s1: Symbol(),
  s2: Symbol(s2),
  hobbies: [ '唱歌', '跳舞', '画画' ],
  hobby: [Function: hobby],
  set: Set(2) { 'aaa', 'bbb' },
  map: Map(3) { '1' => '111', '3' => '333', 'obj' => { o: 0 } },
  info: [Circular *1],
  [Symbol()]: 's1',
  [Symbol(s2)]: 's2'
}

欢迎指正补充!