# 自定义深拷贝函数

  • 对象相互赋值的一些关系,分别包括:
  • 引入的赋值:指向同一个对象,相互之间会影响;
  • 对象的浅拷贝:只是浅层的拷贝,内部引入对象时,依然会相互影响;
  • 对象的深拷贝:两个对象不再有任何关系,不会相互影响;
  • 我们可以通过一种方法来实现深拷贝了:JSON.parse
    • 这种深拷贝的方式其实对于函数、Symbol 等是无法处理的;
    • 并且如果存在对象的循环引用,也会报错的;
  • 自定义深拷贝函数:
    • 自定义深拷贝的基本功能
    • 对 Symbol 的 key 进行处理
    • 其他数据类型的值进程处理:数组、函数、Symbol、Set、Map
    • 对循环引用的处理

# JSON 实现伪深拷贝

  • 拿 lain 对象举栗
const symbol = Symbol()
const lain = {
  name: "lain",
  friend: {
    name: "saber"
  },
  foo() {
    console.log("foo~")
  },
  [symbol]: symbol,
  symbol: symbol
}
  • 使用 JSON 进行深拷贝
const newLain = JSON.parse(JSON.stringify(lain))
console.log(lain)
/*
{
  name: 'lain',
  friend: { name: 'saber' },
  foo: [Function: foo],
  symbol: Symbol(),
  [Symbol()]: Symbol()
}	
*/
console.log(newLain) // { name: 'lain', friend: { name: 'saber' } }
  • 从上面输出可以看出是有很多弊端的

    • 对函数是没办法处理的
    • 对 Symbol 也是无法处理的
    • 并且无法将 lain 指向自己
    lain.lain = lain // 从逻辑上看是没有问题的,但实际上这样会报错

# deepClone v1 基本实现

  • if (!(isObject(originValue))) return originValue 这句代码最主要是用来终止 deepClone 函数的递归调用
// 用于判断是否是 对象类型或者函数类型
function isObject(value) {
  const valueType = typeof value
  return (valueType !== null && typeof value === 'object' || typeof value === 'function' )
}
function deepClone(originValue) {
  // 如果不是对象类型则直接将当前值返回
  if (!(isObject(originValue))) return originValue
  const newObject = {}
  for (const key in originValue) {
    newObject[key] = deepClone(originValue[key])
  }
  return newObject
}
  • 测试对象
const lain = {
  name: "lain",
  friend: {
    name: "saber",
    fruits: ['cherry', 'peack', 'watermelon'],
    friend: {
      name: '樱岛麻衣',
      foo() {
        console.log(`樱岛麻衣 foo~`);
      },
    },
    foo() {
      console.log("saber foo~")
    }
  },
  foo() {
    console.log("lain foo~")
  },
}
  • 测试代码
const newLain = deepClone(lain)
console.log(lain)
/*
{
  name: 'lain',
  friend: {
    name: 'saber',
    fruits: ['cherry', 'peack', 'watermelon'],       
    friend: { name: ' 樱岛麻衣 ', foo: [Function: foo] },
    foo: [Function: foo]
  },
  foo: [Function: foo]
}
*/
console.log(newLain)
/*
{
  name: 'lain',
  friend: {
    name: 'saber',
    fruits: {'0': 'cherry', '1': 'peack', '2': 'watermelon'},
    friend: { name: ' 樱岛麻衣 ', foo: {} },
    foo: {}
  },
  foo: {}
}
*/
  • 从上面来看确实是实现了深拷贝,但是限制新对象与原对象是有些许差异的
    • 比如数组 fruits ,原对象是一个数组,有三个元素,而新对象的 fruits 已经变为了一个对象,用索引作为 key ,用 value 作为值
    • 还有函数也变为了对象
    • 没有对 Symbol 类型进行处理
    • 也没有对 Set/Map 进行处理
  • 所以接下来我会进一步进行完善深拷贝的其他类型判断

# deepClone v2 其他类型

  • 这里进行了非常多的判断逻辑,都是比较简单的
  • 可能对于 Symbol/Set/Map 类型的深拷贝会稍微复杂些
// 这里重写了用 is 对象来判断类型
const is = {
  Array: Array.isArray,
  Date: (val) => val instanceof Date,
  Set: (val) => Object.prototype.toString.call(val) === '[object Set]',
  Map: (val) => Object.prototype.toString.call(val) === '[object Map]',
  Object: (val) => Object.prototype.toString.call(val) === '[object Object]',
  Symbol: (val) => Object.prototype.toString.call(val) === '[object Symbol]',
  Function: (val) => Object.prototype.toString.call(val) === '[object Function]',
}
function deepClone(value) {
  // 2.1 函数浅拷贝
  /* if (is.Function(value)) return value */
    
  // 2.2 函数深拷贝
    if (is.Function(value)) {
    if (/^function/.test(value.toString()) || /^\(\)/.test(value.toString())) 
      return new Function('return ' + value.toString())()
    return new Function('return function ' + value.toString())()
  }
  
  // 3.Date 深拷贝
  if (is.Date(value)) return new Date(value.valueOf())
  // 4. 判断如果是 Symbol 的 value, 那么创建一个新的 Symbol
  if (is.Symbol(value)) return Symbol(value.description)
  // 5. 判断是否是 Set 类型 进行深拷贝
  if (is.Set(value)) {
    // 5.1 浅拷贝 直接进行解构即可
    // return new Set([...value])
    // 5.2 深拷贝
    const newSet = new Set()
    for (const item of value) newSet.add(deepClone(item))
    return newSet
  }
  
  // 6. 判断是否是 Map 类型 
  if (is.Map(value)) {
    // 6.1 浅拷贝 直接进行解构即可
    // return new Map([...value])
    // 6.2 深拷贝
    const newMap = new Map()
    for (const item of value) newMap.set(deepClone(item[0]), deepClone(item[1]))
    return newMap
  }
  // 1. 如果不是对象类型则直接将当前值返回
  if (!(is.Object(value))) return value
  // 7. 判断传入的对象是数组,还是对象
  const newObject = is.Array(value) ? [] : {}
  for (const key in value) {
    // 8 进行递归调用
    newObject[key] = deepClone(value[key])
  }
  // 4.1 对 Symbol 作为 key 进行特殊的处理 拿到对象上面的所有 Symbol key,以数组形式返回
  const symbolKeys = Object.getOwnPropertySymbols(value)
  for (const sKey of symbolKeys) {
    // 4.2 这里没有必要创建一个新的 Symbol
    // const newSKey = Symbol(sKey.description)
    // 4.3 直接将原来的 Symbol key 拷贝到新对象上就可以了
    newObject[sKey] = deepClone(value[sKey])
  }
  return newObject
}

# 测试目标对象

const symboLain = Symbol('lain')
const symboSaber = Symbol('saber')
const lain = {
  name: "lain",
  friend: {
    name: "saber",
    fruits: ['cherry', 'peack', 'watermelon'],
    friend: {
      name: '樱岛麻衣',
      foo() {
        console.log(`樱岛麻衣 foo~`);
      },
    },
    foo() {
      console.log("saber foo~")
    }
  },
  foo() {
    console.log("lain foo~")
  },
  [new Date]: new Date(),
  [symboLain]: symboLain,
  [symboSaber]: symboSaber,
  symboLain: symboSaber,
  set: new Set(['入间同学', '蝶祈', '枫',  {a: 1, b: 2}]),
  map: new Map([['age1', 16], ['age2', 17], ['age3', 18], ['obj', { a: 1, b: 2 }]]),
}

# 测试 Function

console.log(lain.foo === newLain.foo) // false
console.log(lain.friend.foo === newLain.friend.foo) // false
console.log(lain.friend.friend.foo === newLain.friend.friend.foo) // false

# 测试 Date

console.log(lain[new Date]) // 可自行测试
console.log(newLain[new Date]) // 时间也与上面一样 可自行测试
console.log(lain[new Date] === newLain[new Date]) // false

# 测试 Symbol

console.log(lain)
/*
{
  name: 'lain',
  friend: {
    name: 'saber',
    fruits: ['cherry', 'peack', 'watermelon'],
    friend: { name: ' 樱岛麻衣 ', foo: [Function: foo] },
    foo: [Function: foo]
  },
  foo: [Function: foo],
  'Sun Feb 13 2022 14:52:05 GMT+0800 (中国标准时间)': 2022-02-13T06:52:05.892Z,
  symboLain: Symbol (saber),
  set: Set (4) { ' 入间同学 ', ' 蝶祈 ', ' 枫 ', { a: 1, b: 2 } },
  map: Map (4) {
    'age1' => 16,
    'age2' => 17,
    'age3' => 18,
    'obj' => { a: 1, b: 2 }
  },
  [Symbol (lain)]: Symbol (lain),
  [Symbol (saber)]: Symbol (saber)
}
*/
console.log(newLain)
/*
{
  name: 'lain',
  friend: {
    name: 'saber',
    fruits: ['cherry', 'peack', 'watermelon'],
    friend: { name: ' 樱岛麻衣 ', foo: [Function: foo] },
    foo: [Function: foo]
  },
  foo: [Function: foo],
  'Sun Feb 13 2022 14:52:05 GMT+0800 (中国标准时间)': 2022-02-13T06:52:05.892Z,
  symboLain: Symbol (saber),
  set: Set (4) { ' 入间同学 ', ' 蝶祈 ', ' 枫 ', { a: 1, b: 2 } },
  map: Map (4) {
    'age1' => 16,
    'age2' => 17,
    'age3' => 18,
    'obj' => { a: 1, b: 2 }
  },
  [Symbol (lain)]: Symbol (lain),
  [Symbol (saber)]: Symbol (saber)
}
*/

# 测试 Set

lain.set.forEach(item => {
  if (is.Object(item)) {
    // 这里对 lain 中 set 属性中的对象进行添加属性
    item.c = 3
  }
})
console.log(lain.set) // Set (4) { ' 入间同学 ', ' 蝶祈 ', ' 枫 ', { a: 1, b: 2, c: 3 } }
console.log(newLain.set) // Set (4) { ' 入间同学 ', ' 蝶祈 ', ' 枫 ', { a: 1, b: 2 } }

# 最后对 Map 进行代码测试

lain.map.forEach(item => {
  if (is.Object(item)) {
    // 这里对 lain 中 map 属性中的对象进行添加属性
    item.c = 3
  }
})
console.log(lain.map)
/*
Map(4) {
  'age1' => 16,
  'age2' => 17,
  'age3' => 18,
  'obj' => { a: 1, b: 2, c: 3 }
}	
*/
console.log(newLain.map)
/*
Map(4) {
  'age1' => 16,
  'age2' => 17,
  'age3' => 18,
  'obj' => { a: 1, b: 2 }
}
*/
  • map 也是没有问题的

  • 那么类型篇章算是完结了,但我们此时的代码是有 bug 的,至于是什么 bug ,看下面标题应该就知道了~

# deepClone v3 循环引用

  • 此时我们进行一步操作,用下面的代码对上面对象进行测试会发生什么呢?
lain.lain = lain
const newLain = deepClone(lain)
  • Uncaught RangeError: Maximum call stack size exceeded 没错,会发生栈溢出!
// 用于判断类型
const is = {
  Array: Array.isArray,
  Date: (val) => val instanceof Date,
  Set: (val) => Object.prototype.toString.call(val) === '[object Set]',
  Map: (val) => Object.prototype.toString.call(val) === '[object Map]',
  Object: (val) => Object.prototype.toString.call(val) === '[object Object]',
  Symbol: (val) => Object.prototype.toString.call(val) === '[object Symbol]',
  Function: (val) => Object.prototype.toString.call(val) === '[object Function]',
}
function deepClone(value, weakMap = new WeakMap()) {
  // 2.1 函数浅拷贝
  /* if (is.Function(value)) return value */
    
  // 2.2 函数深拷贝
    if (is.Function(value)) {
    if (/^function/.test(value.toString()) || /^\(\)/.test(value.toString())) 
      return new Function('return ' + value.toString())()
    return new Function('return function ' + value.toString())()
  }
  
  // 3.Date 深拷贝
  if (is.Date(value)) return new Date(value.valueOf())
  // 4. 判断如果是 Symbol 的 value, 那么创建一个新的 Symbol
  if (is.Symbol(value)) return Symbol(value.description)
  // 5. 判断是否是 Set 类型 进行深拷贝
  if (is.Set(value)) {
    // 5.1 浅拷贝 直接进行解构即可
    // return new Set([...value])
    // 5.2 深拷贝
    const newSet = new Set()
    for (const item of value) newSet.add(deepClone(item), weakMap)
    return newSet
  }
  
  // 6. 判断是否是 Map 类型 
  if (is.Map(value)) {
    // 6.1 浅拷贝 直接进行解构即可
    // return new Map([...value])
    // 6.2 深拷贝
    const newMap = new Map()
    for (const item of value) newMap.set(deepClone(item[0], weakMap), deepClone(item[1], weakMap))
    return newMap
  }
  // 9. 判断 weakMap 是否有值 有值的情况下就直接将值返回就可以
  if(weakMap.has(value)) return weakMap.get(value)
  // 1. 如果不是对象类型则直接将当前值返回
  if (!(is.Object(value))) return value
  // 7. 判断传入的对象是数组,还是对象
  const newObj = is.Array(value) ? [] : {}
  // 10. 当 weakMap 没有值时,将 originValue 作为 key, newObj 作为 value
    weakMap.set(value, newObj)
  
  for (const key in value) {
    weakMap.set(value, newObj)
    // 8 进行递归调用
    newObj[key] = deepClone(value[key], weakMap)
  }
  // 4.1 对 Symbol 作为 key 进行特殊的处理 拿到对象上面的所有 Symbol key,以数组形式返回
  const symbolKeys = Object.getOwnPropertySymbols(value)
  for (const sKey of symbolKeys) {
    // 4.2 这里没有必要创建一个新的 Symbol
    // const newSKey = Symbol(sKey.description)
    // 4.3 直接将原来的 Symbol key 拷贝到新对象上就可以了
    newObj[sKey] = deepClone(value[sKey], weakMap)
  }
  return newObj
}
  • 继续用上面的代码进行测试,建议用 浏览器测试
lain.lain = lain
const newLain = deepClone(lain)
// 没有溢栈 可自行去测
console.log(lain) 
console.log(newLain)
  • 此时代码就没有问题了,那么深拷贝篇到此就快结束了~

# deepClone v4 仿源码版

  • 阅读了 vuex 源码并自己实现了 vuex 后,我也对深拷贝进阶的实现了下
function isFunction(val) {
  return Object.prototype.toString.call(val) === '[object Function]'
}
function isObject(val) {
  return Object.prototype.toString.call(val) === '[object Object]'
}
function isArray(val) {
  return Object.prototype.toString.call(val) === '[object Array]'
}
function isSet(val) {
  return Object.prototype.toString.call(val) === '[object Set]'
}
function isMap(val) {
  return Object.prototype.toString.call(val) === '[object Map]'
}
function isSymbol(val) {
  return Object.prototype.toString.call(val) === '[object Symbol]'
}
function isDate(val) {
  return Object.prototype.toString.call(val) === '[object Date]'
}
function ArrayBuffer(val) {
  return Object.prototype.toString.call(val) === '[object ArrayBuffer]'
}
const forEachValue = (obj, fn) =>  Object.keys(obj).forEach(key => fn(obj[key], key))
  
function deepClone(val, weakMap = new WeakMap()) {
  if (isDate(val)) return new Date(+val)
    
  if (isMap(val)) {
    const map = new Map()
    for (const item of val) map.set(deepClone(item[0], weakMap), deepClone(item[1], weakMap))
    return map
  }
  if (isSet(val)) {
    const set = new Set()
    val.forEach(item => set.add(deepClone(item), weakMap))
    return set
  }
  if (isSymbol(val)) return Symbol(val.description)
  if (isFunction(val)) {
    if (/^function|^\(\)/.test(val.toString())) {
      return new Function(`return ${val.toString()}`)()
    } else {
      return new Function(`return function ${val.toString()}`)()
    }
  }
    
  if (!isObject(val)) return val
  
  const obj = isArray(val) ? [] : {}
  if(weakMap.has(val)) return weakMap.get(val)
  weakMap.set(val, obj)
  forEachValue(val, (val, key) => obj[key] = deepClone(val, weakMap))
  const symbols = Object.getOwnPropertySymbols(val)
  forEachValue(symbols, key => obj[Symbol(key.description)] = deepClone(symbols[key], weakMap))
  return obj
}