3

leader:深拷贝有这5个段位,你只是青铜段位?还想涨薪?

 2 years ago
source link: https://segmentfault.com/a/1190000041008071
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

大家好,我是林三心。前几天跟leader在聊深拷贝

  • leader:你知道怎么复制一个对象吗?
  • 我:知道啊!不就深拷贝吗?
  • leader:那你是怎么深拷贝的?
  • 我:我直接一手JSON.parse(JSON.stringfy(obj))吃遍天
  • leader:兄弟,有空去看看lodash里的deepClone,看看人家是怎么实现的

哈哈,确实,深拷贝在日常开发中是有很多应用场景的,他也是非常重要的,写一个合格的深拷贝方法是很有必要的。那怎么才能写一个合格的深拷贝方法呢?或者说,怎么才能写一个毫无破绽的深拷贝方法呢?

image.png

深拷贝 && 浅拷贝

咱们先来说说什么是深拷贝,什么是浅拷贝吧。

浅拷贝

所谓浅拷贝,就是只复制最外一层,里面的都还是相同引用

// 浅拷贝
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

深拷贝

深拷贝,则是你将一个对象拷贝到另一个新变量,这个新变量指向的是一块新的堆内存地址

// 深拷贝

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

相信大多数人平时在实现深拷贝时,都会这么去实现

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

虽然大多数时候这么使用是没问题的,但这种方式还是有很多缺点的

  • 1、对象中有字段值为undefined,转换后则会直接字段消失
  • 2、对象如果有字段值为RegExp对象,转换后则字段值会变成{}
  • 3、对象如果有字段值为NaN、+-Infinity,转换后则字段值变成null
  • 4、对象如果有环引用,转换直接报错

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

既然是要对象的深拷贝,那我可以创建一个空对象,并把需要拷贝的原对象的值一个一个复制过来就可以了呀!!!

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

但是其实上面这种做法是不完善的,因为咱们根本不知道咱们想拷贝的对象有多少层。。大家一听到“不知道有多少层”,想必就会想到递归了吧,是的,使用递归就可以了。

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

前面咱们只考虑了对象的情况,但是没把数组情况也给考虑,所以咱们要加上数组条件

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

前面实现的方法都没有解决环引用的问题

  • JSON.parse(JSON.stringify(target))报错TypeError: Converting circular structure to JSON,意思是无法处理环引用
  • 递归方法报错Maximum call stack size exceeded,意思是递归不完,爆栈

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

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

那怎么解决环引用呢?其实说难也不难,需要用到ES6的数据结构Map

  • 每次遍历到有引用数据类型,就把他当做key放到Map中,对应的value是新创建的对象temp
  • 每次遍历到有引用数据类型,就去Map中找找有没有对应的key,如果有,就说明这个对象之前已经注册过,现在又遇到第二次,那肯定就是环引用了,直接根据key获取value,并返回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

image.png

刚刚咱们只是实现了

  • 基本数据类型的拷贝
  • 引用数据类型中的数组,对象

但其实,引用数据类型可不止只有数组和对象,我们还得解决以下的引用类型的拷贝问题,那怎么判断每个引用数据类型的各自类型呢?可以使用Object.prototype.toString.call()

类型toString结果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]

我们先把以上的引用类型数据分为两类

  • 可遍历的数据类型
  • 不可遍历的数据类型
// 可遍历的类型
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()
}

可遍历引用类型

主要处理以下四种类型

  • 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

不可遍历引用类型

主要处理以下几种类型

  • Symbol
  • RegExp
  • Function

先把拷贝这三个类型的方法写出来

// 拷贝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;
}

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

如果你觉得此文对你有一丁点帮助,点个赞,鼓励一下林三心哈哈。或者可以加入我的摸鱼群
想进学习群,摸鱼群,请点击这里[摸鱼](
https://juejin.cn/pin/6969565...),我会定时直播模拟面试,答疑解惑

image.png


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK