15
头图

Preface

Hi everyone, this is Lin Sanxin. I was chatting with the leader a few days ago about deep copy

  • leader: Do you know how to copy an object?
  • I know! Isn't it a deep copy of ?
  • leader: Then how did you deep copy of 161b1b2ab39ab4?
  • Me: I JSON.parse(JSON.stringfy(obj)) eat 061b1b2ab39acc directly
  • leader: Brother, if lodash have time, deepClone and see 061b1b2ab39af1 in 061b1b2ab39af0 to see how others achieve it

Haha, indeed, deep copy has many application scenarios in daily development, and it is also very important. It is necessary to deep copy method. So how can we write a qualified deep copy method? In other words, how can we write a deep copy method?

image.png

Deep copy && Shallow copy

Let's first talk about what is a deep copy and what is a shallow copy.

Shallow copy

The so-called shallow copy means that only the outermost layer is copied, and the contents inside are still the same reference

// 浅拷贝
const a = { name: 'sunshine_lin', age: 23, arr: [] }
const b = {}
for (let key in a){
    b[key] = a[key]
}

console.log(b) // { name: 'sunshine_lin', age: 23, arr: [] }
console.log(b === a) // false
console.log(b.arr === a.arr) // true

deep copy

Deep copy, you copy an object to another new variable, this new variable points to a new heap memory address

// 深拷贝

function deepClone(target) {
    // ...实现深拷贝
}

const a = { name: 'sunshine_lin', age: 23, arr: [] }
const b = deepClone(a)

console.log(b) // { name: 'sunshine_lin', age: 23, arr: [] }
console.log(b === a) // false
console.log(b.arr === a.arr) // false

Gold version

I believe that most people will do this when they implement deep copy.

function deepClone(target) {
    return JSON.parse(JSON.stringify(target))
}

const a = { name: 'sunshine_lin', age: 23 }
const b = deepClone(a)

console.log(b) // { name: 'sunshine_lin', age: 23 }
console.log(b === a) // false

Although it is fine to use this way most of the time, there are still many shortcomings in this method.

  • 1. There is a field value of undefined object, and the field will disappear directly after conversion
  • 2. If the object has a field value of RegExp object, the field value will become {} after conversion
  • 3. If the object has a field value of NaN、+-Infinity , the field value becomes null after conversion
  • 4. If the object has reference, the conversion will report an error directly

截屏2021-10-02 下午9.34.22.png

Platinum version

Since it is a deep copy of the object, I can create an empty object and copy the values of the original objects that need to be copied one by one! ! !

function deepClone(target) {
    const temp = {}
    for (const key in target) {
        temp[key] = target[key]
    }
    return temp
}

const a = { name: 'sunshine_lin', age: 23 }
const b = deepClone(a)

console.log(b) // { name: 'sunshine_lin', age: 23 }
console.log(b === a) // false

But in fact, the above approach is imperfect, because we don't know how many layers of objects we want to copy. . When everyone hears "I don't know how many layers there are," they must think of recursion. Yes, just use recursion.

function deepClone(target) {
    // 基本数据类型直接返回
    if (typeof target !== 'object') {
        return target
    }

    // 引用数据类型特殊处理
    const temp = {}
    for (const key in target) {
        // 递归
        temp[key] = deepClone(target[key])
    }
    return temp
}

const a = {
    name: 'sunshine_lin',
    age: 23,
    hobbies: { sports: '篮球',tv: '雍正王朝' }
}
const b = deepClone(a)

console.log(b)
// {
//     name: 'sunshine_lin',
//     age: 23,
//     hobbies: { sports: '篮球', tv: '雍正王朝' }
// }
console.log(b === a) // false

Diamond version

Earlier, we only considered the case of objects, but did not consider the case of arrays, so we have to add array conditions

function deepClone(target) {
    // 基本数据类型直接返回
    if (typeof target !== 'object') {
        return target
    }

    // 引用数据类型特殊处理
    
    // 判断数组还是对象
    const temp = Array.isArray(target) ? [] : {}
    for (const key in target) {
        // 递归
        temp[key] = deepClone(target[key])
    }
    return temp
}

const a = {
    name: 'sunshine_lin',
    age: 23,
    hobbies: { sports: '篮球', tv: '雍正王朝' },
    works: ['2020', '2021']
}
const b = deepClone(a)

console.log(b)
// {
//     name: 'sunshine_lin',
//     age: 23,
//     hobbies: { sports: '篮球', tv: '雍正王朝' },
//     works: ['2020', '2021']
// }
console.log(b === a) // false

Star version

None of the previously implemented methods solve the problem of ring reference

  • JSON.parse(JSON.stringify(target)) error TypeError: Converting circular structure to JSON , which means that cannot handle ring references
  • recursive method Maximum call stack size exceeded error 061b1b2ab39ead, which means that cannot finish recursively, and the stack bursts

截屏2021-10-02 下午10.06.58.png

// 环引用
const a = {}
a.key = a

How to resolve ring references? In fact, it is not difficult to say, you need to use the ES6 data structure Map

  • Every time a reference data type is traversed, it is treated as key placed in Map . The corresponding value is the newly created object temp
  • Every time a reference data type is traversed, look for the corresponding key . If there is, it means that the object has been registered before, and now it is encountered for the second time, it must be a ring reference. key gets value and returns value

截屏2021-10-02 下午10.18.19.png

function deepClone(target, map = new Map()) {
    // 基本数据类型直接返回
    if (typeof target !== 'object') {
        return target
    }

    // 引用数据类型特殊处理
    // 判断数组还是对象
    const temp = Array.isArray(target) ? [] : {}

+    if (map.get(target)) {
+        // 已存在则直接返回
+        return map.get(target)
+    }
+    // 不存在则第一次设置
+    map.set(target, temp)

    for (const key in target) {
        // 递归
        temp[key] = deepClone(target[key], map)
    }
    return temp
}

const a = {
    name: 'sunshine_lin',
    age: 23,
    hobbies: { sports: '篮球', tv: '雍正王朝' },
    works: ['2020', '2021']
}
a.key = a // 环引用
const b = deepClone(a)

console.log(b)
// {
//     name: 'sunshine_lin',
//     age: 23,
//     hobbies: { sports: '篮球', tv: '雍正王朝' },
//     works: [ '2020', '2021' ],
//     key: [Circular]
// }
console.log(b === a) // false

King version

image.png

We just realized

  • copy of basic data types
  • refers to the array, object in the data type

But in fact, reference data types are more than just arrays and objects. We have to solve the following problem of copying reference types. How to judge the respective types of each reference data type? Can use Object.prototype.toString.call()

typetoStringresult
MapObject.prototype.toString.call(new Map())[object Map]
SetObject.prototype.toString.call(new Set())[object Set]
ArrayObject.prototype.toString.call([])[object Array]
ObjectObject.prototype.toString.call({})[object Object]
SymbolObject.prototype.toString.call(Symbol())[object Symbol]
RegExpObject.prototype.toString.call(new RegExp())[object RegExp]
FunctionObject.prototype.toString.call(function() {})[object Function]

Let's first divide the above reference type data into two categories

  • Traversable data types
  • Non-traversable data types
// 可遍历的类型
const mapTag = '[object Map]';
const setTag = '[object Set]';
const arrayTag = '[object Array]';
const objectTag = '[object Object]';

// 不可遍历类型
const symbolTag = '[object Symbol]';
const regexpTag = '[object RegExp]';
const funcTag = '[object Function]';

// 将可遍历类型存在一个数组里
const canForArr = ['[object Map]', '[object Set]',
                   '[object Array]', '[object Object]']

// 将不可遍历类型存在一个数组
const noForArr = ['[object Symbol]', '[object RegExp]', '[object Function]']

// 判断类型的函数
function checkType(target) {
    return Object.prototype.toString.call(target)
}

// 判断引用类型的temp
function checkTemp(target) {
    const c = target.constructor
    return new c()
}

Traversable reference types

Mainly deal with the following four types

  • Map
  • Set
  • Object
  • Array

    function deepClone(target, map = new Map()) {
    
      // 获取类型
      const type = checkType(target)
    
    
      // 基本数据类型直接返回
      if (!canForArr.concat(noForArr).includes(type)) {
          return target
      }
    
      // 引用数据类型特殊处理
      const temp = checkTemp(target)
    
      if (map.get(target)) {
          // 已存在则直接返回
          return map.get(target)
      }
      // 不存在则第一次设置
      map.set(target, temp)
      
      // 处理Map类型
      if (type === mapTag) {
          target.forEach((value, key) => {
              temp.set(key, deepClone(value, map))
          })
    
          return temp
      }
    
      // 处理Set类型
      if (type === setTag) {
          target.forEach(value => {
              temp.add(deepClone(value, map))
          })
    
          return temp
      }
      
      // 处理数据和对象
      for (const key in target) {
          // 递归
          temp[key] = deepClone(target[key], map)
      }
      return temp
    }
    
    
    const a = {
      name: 'sunshine_lin',
      age: 23,
      hobbies: { sports: '篮球', tv: '雍正王朝' },
      works: ['2020', '2021'],
      map: new Map([['haha', 111], ['xixi', 222]]),
      set: new Set([1, 2, 3]),
    }
    a.key = a // 环引用
    const b = deepClone(a)
    
    console.log(b)
    // {
    //     name: 'sunshine_lin',
    //     age: 23,
    //     hobbies: { sports: '篮球', tv: '雍正王朝' },
    //     works: [ '2020', '2021' ],
    //     map: Map { 'haha' => 111, 'xixi' => 222 },
    //     set: Set { 1, 2, 3 },
    //     key: [Circular]
    // }
    console.log(b === a) // false

Non-traversable reference types

Mainly deal with the following types

  • Symbol
  • RegExp
  • Function

Write out the methods of copying these three types first

// 拷贝Function的方法
function cloneFunction(func) {
    const bodyReg = /(?<={)(.|\n)+(?=})/m;
    const paramReg = /(?<=\().+(?=\)\s+{)/;
    const funcString = func.toString();
    if (func.prototype) {
        const param = paramReg.exec(funcString);
        const body = bodyReg.exec(funcString);
        if (body) {
            if (param) {
                const paramArr = param[0].split(',');
                return new Function(...paramArr, body[0]);
            } else {
                return new Function(body[0]);
            }
        } else {
            return null;
        }
    } else {
        return eval(funcString);
    }
}

// 拷贝Symbol的方法
function cloneSymbol(targe) {
    return Object(Symbol.prototype.valueOf.call(targe));
}

// 拷贝RegExp的方法
function cloneReg(targe) {
    const reFlags = /\w*$/;
    const result = new targe.constructor(targe.source, reFlags.exec(targe));
    result.lastIndex = targe.lastIndex;
    return result;
}

Final version

image.png

function deepClone(target, map = new Map()) {

    // 获取类型
    const type = checkType(target)


    // 基本数据类型直接返回
    if (!canForArr.concat(noForArr).includes(type)) return target


    // 判断Function,RegExp,Symbol
  +  if (type === funcTag) return cloneFunction(target)
  +  if (type === regexpTag) return cloneReg(target)
  +  if (type === symbolTag) return cloneSymbol(target)

    // 引用数据类型特殊处理
    const temp = checkTemp(target)

    if (map.get(target)) {
        // 已存在则直接返回
        return map.get(target)
    }
    // 不存在则第一次设置
    map.set(target, temp)

    // 处理Map类型
    if (type === mapTag) {
        target.forEach((value, key) => {
            temp.set(key, deepClone(value, map))
        })

        return temp
    }

    // 处理Set类型
    if (type === setTag) {
        target.forEach(value => {
            temp.add(deepClone(value, map))
        })

        return temp
    }

    // 处理数据和对象
    for (const key in target) {
        // 递归
        temp[key] = deepClone(target[key], map)
    }
    return temp
}


const a = {
    name: 'sunshine_lin',
    age: 23,
    hobbies: { sports: '篮球', tv: '雍正王朝' },
    works: ['2020', '2021'],
    map: new Map([['haha', 111], ['xixi', 222]]),
    set: new Set([1, 2, 3]),
    func: (name, age) => `${name}今年${age}岁啦!!!`,
    sym: Symbol(123),
    reg: new RegExp(/haha/g),
}
a.key = a // 环引用

const b = deepClone(a)
console.log(b)
// {
//     name: 'sunshine_lin',
//     age: 23,
//     hobbies: { sports: '篮球', tv: '雍正王朝' },
//     works: [ '2020', '2021' ],
//     map: Map { 'haha' => 111, 'xixi' => 222 },
//     set: Set { 1, 2, 3 },
//     func: [Function],
//     sym: [Symbol: Symbol(123)],
//     reg: /haha/g,
//     key: [Circular]
// }
console.log(b === a) // false

Concluding remarks

If you think this article is of little help to you, please give me a thumbs up and encourage Lin Sanxin haha. Or you can join my fishing group, we study hard together, ah ah ah ah ah, I will mock interviews regularly, resume guidance, answer questions and answer questions, let's learn from each other and make progress together! !
截屏2021-11-28 上午9.43.19.png


Sunshine_Lin
2.1k 声望7.1k 粉丝