手撕JavaScript深拷贝&浅拷贝

290 阅读5分钟

深拷贝要考虑到所有数据类型

var obj = {
    name: 'name',
    age: 30,
    bool: true,
    n: null,
    u: undefined,
    symb: Symbol('symbol'),
    big: BigInt(12),
    arr: [1, 2, 3],
    reg: /^\d+$/,
    fn: function() {
        console.log('fn')
    },
    time: new Date(),
    err: new Error('error'),
    set: new Set([{a: 1}, {a: 2}]),
    map: new Map([[{a: 1}, {b: 1}], [{a: 2}, {b: 2}]]),
    child: {
        ele: 'body',
        x: 100
    }
};

// 测试循环依赖
// obj.obj = obj;
  • 7基本数据类型:string、number、boolean、null、undefined、symbol、BigInt
  • symbol类型做value、symbol类型做key
  • 引用类型:数组、正则、日期、Error、Set、Map
  • 函数类型
  • 深层次对象类型
  • 本文没有考虑ArrayBuffer这种引用数据类型;lodash的深拷贝中会处理

使用展开运算符完成对象拷贝存在的问题

image.png

  • 展开运算符可以把obj的每一项展开然后赋值给obj2

image.png

  • 因为obj2在堆内存中开辟了一份新的内存空间来存储从obj拷贝过来的所有字段的值
  • obj2 === obj => false,因为是一个新的对象,所以不相等

image.png

  • obj2中的child和arr又是引用类型,内部还包含其他基本数据类型或者引用类型
  • obj2.child === obj.child => true
  • ...展开运算符是浅拷贝,只拷贝了obj的第一级的key对应的值给到obj2,第二级以及后面的第三级、第四级没有进行拷贝;所以obj.child 和 obj2.child指向的是同一块内存

使用JSON.stringfy进行深拷贝存在的问题

image.png

  • BigInt类型的值不能被序列化

image.png

  • 把BigInt类型的值先去掉,就能使用JSON.stringfy来深拷贝
  • undefined类型的值丢了
  • 函数类型的值丢了
  • Symbol类型的值丢了
  • 正则类型的值变成空对象了
  • Error类型的值变成空对象了
  • Set类型的值变成空对象了
  • Map类型的值变成空对象了
  • key是Symbol类型的话,该key都丢了,即使自己for in循序也不能遍历Symbol类型的key
  • Date日期类型的值变成字符串了

JSON.stringify会把值变成字符串,对于特殊的数据类型就是转不了字符串在反序列化回来,所以就会存在问题

image.png

  • JSON.parse(JSON.stringify(obj))先stringify再parse得到的对象确实是深拷贝
  • 第二级child也进行深拷贝了

第一版深拷贝实现

 function deepClone(obj){
    if(obj == null) return obj;
    if(typeof obj !=='object') return obj;
    let cloneObj = new obj.constructor
    for(let key in obj){
        if(obj.hasOwnProperty(key)){
            cloneObj[key] = deepClone(obj[key])
        }
    }
    return cloneObj
}

基本思路

  • 如果是基本数据类型直接把原始的值赋给新的克隆后的对象相应的key
  • 如果是引用类型的值,就new一个新的该引用类型对象,然后值赋给新的克隆后的对象相应的key

存在的问题

image.png

  • 正则表达式的内容丢失
  • error对象的message丢失
  • set对象长度为0
  • map对象长度为0
  • function函数类型的值因为typeof返回的是'function'并不是'object',所以直接返回了

第二版深拷贝实现

function deepClone(target){
           
    // 数据类型
    let type = Object.prototype.toString.call(target).slice(8, -1).toLowerCase();

    // 处理null和undefined, null == undefined
    if (target == null) return target;

    // constructor构造函数, 必须要放
    let ctr = target.constructor;

    // Regexp正则和Date日期类型
    if (/^(regexp|date)$/i.test(type)) {
        return new ctr(target);
    }
    // Error类型
    if (/^(error)$/i.test(type)) {
        return new ctr(target.message);
    }
    // Function类型
    if (/^(function)$/i.test(type)) {
        return function proxy(...args) {
            target.call(this, args);
        };
    }
    // Set类型
    if (/^(set)$/i.test(type)) {
        let cloneSet = new ctr;
        target.forEach((item) => {
            cloneSet.add(_deepClone(item))
        })
        return cloneSet;
    }

    // Map类型
    if (/^(map)$/i.test(type)) {
        let cloneMap = new ctr;
        target.forEach((item, key) => {
            cloneMap.set(_deepClone(key), _deepClone(item))
        })
        return cloneMap;
    }

    // Symbol不用处理,因为Symbol(xxx)调用后返回的值不相等
    // 其他数据类型原样返回,后续有处理的其他数据类型如ArrayBuffer就可以在上面继续添加
    if (!/^(array|object)$/i.test(type)) return target;

    let cloneTarget = new ctr
    for(let key in target){
        if(target.hasOwnProperty(key)){
            cloneTarget[key] = deepClone(target[key]);
        }
    }
    return cloneTarget;
}

image.png

  • 正则类型的值只需要把原本的正则target传给新的构造函数就不会丢失,而且还创一个新的实例
  • Error类型需要把message传给构造函数
  • Funtion类型需要Object.prototype.toString.call(target).slice(8, -1).toLowerCase()来拿到类型的相信信息,从而对函数类型的值单独处理,不能直接返回

第三版深拷贝实现-循环依赖

 var obj = {
    name: 'name',
    age: 30,
    bool: true,
    n: null,
    u: undefined,
    symb: Symbol('symbol'),
    big: BigInt(12),
    arr: [1, 2, 3],
    reg: /^\d+$/,
    fn: function() {
        console.log('fn')
    },
    time: new Date(),
    err: new Error('error'),
    set: new Set([{a: 1}, {a: 2}]),
    map: new Map([[{a: 1}, {b: 1}], [{a: 2}, {b: 2}]]),
    child: {
        ele: 'body',
        x: 100
    }
};

// 循环依赖
obj.obj = obj;

在控制台可以无限查看obj,这就是循环依赖

image.png

如果沿用原来的代码会报调用次数太多,爆栈 Maximum call stack size exceeded

image.png

解决思路

  • 避免对象或者数组类型的值循环递归

方案一

  • 利用缓存,如果某个对象或者数组类型的值已经处理过就直接返回,然后就不会进入无限递归了
  • 采用WeakMap,是因为弱引用,使用完就被垃圾回收了,避免递归内存溢出
function deepClone(target, map = new WeakMap()){
           
    // 数据类型
    let type = Object.prototype.toString.call(target).slice(8, -1).toLowerCase();

    // 处理null和undefined, null == undefined
    if (target == null) return obj;

    // constructor构造函数, 必须要放
    let ctr = target.constructor;

    // Regexp正则和Date日期类型
    if (/^(regexp|date)$/i.test(type)) {
        return new ctr(target);
    }
    // Error类型
    if (/^(error)$/i.test(type)) {
        return new ctr(target.message);
    }
    // Function类型
    if (/^(function)$/i.test(type)) {
        return function proxy(...args) {
            target.call(this, args);
        };
    }
    
    // Set类型
    if (/^(set)$/i.test(type)) {
        let cloneSet = new ctr;
        target.forEach((item) => {
            cloneSet.add(_deepClone(item))
        })
        return cloneSet;
    }

    // Map类型
    if (/^(map)$/i.test(type)) {
        let cloneMap = new ctr;
        target.forEach((item, key) => {
            cloneMap.set(_deepClone(key), _deepClone(item))
        })
        return cloneMap;
    }

    // Symbol不用处理,因为Symbol(xxx)调用后返回的值不相等
    // 其他数据类型原样返回,后续有处理的其他数据类型如ArrayBuffer就可以在上面继续添加
    if (!/^(array|object)$/i.test(type)) return target;

    // map中有就直接返回,不要递归了,就不会无限递归了
    if(map.get(target)) return map.get(target);

    let cloneTarget = new ctr;
    // 存放对象或数组类型值的拷贝结果
    map.set(target, cloneTarget);
    for(let key in target){
        if(target.hasOwnProperty(key)){
            cloneTarget[key] = deepClone(target[key], map);
        }
    }
    return cloneTarget;
}

方案二

  • 利用WeakSet或数组来存放某个对象或者数组类型的target,发现处理过就直接返回
  • 直接返回
function deepClone(target, set = new WeakSet()){
           
    // 数据类型
    let type = Object.prototype.toString.call(target).slice(8, -1).toLowerCase();

    // 处理null和undefined, null == undefined
    if (target == null) return obj;

    // constructor构造函数, 必须要放
    let ctr = target.constructor;

    // Regexp正则和Date日期类型
    if (/^(regexp|date)$/i.test(type)) {
        return new ctr(target);
    }
    // Error类型
    if (/^(error)$/i.test(type)) {
        return new ctr(target.message);
    }
    // Function类型
    if (/^(function)$/i.test(type)) {
        return function proxy(...args) {
            target.call(this, args);
        };
    }
    
    // Set类型
    if (/^(set)$/i.test(type)) {
        let cloneSet = new ctr;
        target.forEach((item) => {
            cloneSet.add(_deepClone(item))
        })
        return cloneSet;
    }

    // Map类型
    if (/^(map)$/i.test(type)) {
        let cloneMap = new ctr;
        target.forEach((item, key) => {
            cloneMap.set(_deepClone(key), _deepClone(item))
        })
        return cloneMap;
    }

    // Symbol不用处理,因为Symbol(xxx)调用后返回的值不相等
    // 其他数据类型原样返回,后续有处理的其他数据类型如ArrayBuffer就可以在上面继续添加
    if (!/^(array|object)$/i.test(type)) return target;

    // map中有就直接返回,不要递归了,就不会无限递归了
    // 返回target
    // if(set.has(target)) return target;
    // 直接return
    if(set.has(target)) return;

    let cloneTarget = new ctr;
    // 存放对象或数组类型值的拷贝结果
    set.add(target);
    for(let key in target){
        if(target.hasOwnProperty(key)){
            cloneTarget[key] = deepClone(target[key], set);
        }
    }
    return cloneTarget
}
  • 直接返回就相当于没有拷贝

image.png

  • 返回target,相当于没拷贝

image.png

优化传递map

function deepClone(obj){
    let map = new WeakMap();
    const _deepClone = (target) => {
        // 数据类型
        let type = Object.prototype.toString.call(target).slice(8, -1).toLowerCase();

        // 处理null和undefined, null == undefined
        if (target == null) return obj;

        // constructor构造函数, 必须要放
        let ctr = target.constructor;

        // Regexp正则和Date日期类型
        if (/^(regexp|date)$/i.test(type)) {
            return new ctr(target);
        }
        // Error类型
        if (/^(error)$/i.test(type)) {
            return new ctr(target.message);
        }
        // Function类型
        if (/^(function)$/i.test(type)) {
            return function proxy(...args) {
                target.call(this, args);
            };
        }
        
          // Set类型
	    if (/^(set)$/i.test(type)) {
	        let cloneSet = new ctr;
	        target.forEach((item) => {
	            cloneSet.add(_deepClone(item))
	        })
	        return cloneSet;
	    }
	
	    // Map类型
	    if (/^(map)$/i.test(type)) {
	        let cloneMap = new ctr;
	        target.forEach((item, key) => {
	            cloneMap.set(_deepClone(key), _deepClone(item))
	        })
	        return cloneMap;
	    }

        // Symbol不用处理,因为Symbol(xxx)调用后返回的值不相等
        // 其他数据类型原样返回,后续有处理的其他数据类型如ArrayBuffer就可以在上面继续添加
        if (!/^(array|object)$/i.test(type)) return target;

        // map中有就直接返回,不要递归了,就不会无限递归了
        if(map.has(target)) return target;
        // if(set.has(target)) return;

        let cloneTarget = new ctr;
        // 存放对象或数组类型值的拷贝结果
        map.set(target, cloneObj);
        for(let key in target){
            if(target.hasOwnProperty(key)){
                cloneTarget[key] = _deepClone(target[key]);
            }
        }
        return cloneTarget;
    }
    return _deepClone(obj);
    
}

同时支持浅拷贝和深拷贝

function clone(...params) {
    let target = params[0];
    let deep = false;
    let len = params.length;
    // 存放已经处理过的target的result结果
    let map = params[2] || new WeakMap();
    if (typeof target === 'boolean' && len >= 2) {
        deep = target;
        target = params[1];
    }
    // 发现循环依赖就直接返回,避免进入递归,导致爆栈
    if (map.has(target)) return map.get(target);
    

    // 数据类型
    let type = Object.prototype.toString.call(target).slice(8, -1).toLowerCase();

    // 处理null和undefined, null == undefined
    if (target == null) return target;

    // constructor构造函数, 必须要放
    let ctr = target.constructor;

    // Regexp正则和Date日期类型
    if (/^(regexp|date)$/i.test(type)) {
        return new ctr(target);
    }
    // Error类型
    if (/^(error)$/i.test(type)) {
        return new ctr(target.message);
    }
    // Function类型
    if (/^(function)$/i.test(type)) {
        return function proxy(...args) {
            target.call(this, args);
        };
    }
    
    // Set类型
    if (/^(set)$/i.test(type)) {
        let cloneSet = new ctr;
        target.forEach((item) => {
            cloneSet.add(_deepClone(item))
        })
        return cloneSet;
    }
	
    // Map类型
    if (/^(map)$/i.test(type)) {
        let cloneMap = new ctr;
        target.forEach((item, key) => {
            cloneMap.set(_deepClone(key), _deepClone(item))
        })
        return cloneMap;
    }

    // Symbol不用处理,因为Symbol(xxx)调用后返回的值不相等
    // 其他数据类型原样返回,后续有处理的其他数据类型如ArrayBuffer就可以在上面继续添加
    if (!/^(array|object)$/i.test(type)) return target;

    // 处理对象和数组
    let cloneObj = new ctr;
    // 存放该target对应的结果result
    map.set(target, cloneObj);
    // for in可以同时遍历数组和对象
    for (let key in target) {
        if (target.hasOwnProperty(key)) {
            cloneObj[key] = deep ? clone(true, target[key], map) : target[key];
        }
    }
    return cloneObj;
}

let deepCopy = clone(true, obj);
let copy = clone(obj);

函数类型的深拷贝

  • 函数拷贝有两种方案
  • 方案一返回一个代理函数,函数内部还是调用原函数,就是本文用的方案
  • 方案二new Function,方案如下
const getType = (obj) => Object.prototype.toString.call(obj)

const isObject = (target) =>
  (typeof target === "object" || typeof target === "function") &&
  target !== null

const canTraverse = {
  "[object Map]": true,
  "[object Set]": true,
  "[object Array]": true,
  "[object Object]": true,
  "[object Arguments]": true,
}
const mapTag = "[object Map]"
const setTag = "[object Set]"
const boolTag = "[object Boolean]"
const numberTag = "[object Number]"
const stringTag = "[object String]"
const symbolTag = "[object Symbol]"
const dateTag = "[object Date]"
const errorTag = "[object Error]"
const regexpTag = "[object RegExp]"
const funcTag = "[object Function]"

const handleRegExp = (target) => {
  const { source, flags } = target
  return new target.constructor(source, flags)
}

const handleFunc = (func) => {
  // 箭头函数直接返回自身
  if (!func.prototype) return func
  const bodyReg = /(?<={)(.|\n)+(?=})/m
  const paramReg = /(?<=\().+(?=\)\s+{)/
  const funcString = func.toString()
  // 分别匹配 函数参数 和 函数体
  const param = paramReg.exec(funcString)
  const body = bodyReg.exec(funcString)
  if (!body) return null
  if (param) {
    const paramArr = param[0].split(",")
    return new Function(...paramArr, body[0])
  } else {
    return new Function(body[0])
  }
}

const handleNotTraverse = (target, tag) => {
  const Ctor = target.constructor
  switch (tag) {
    case boolTag:
      return new Object(Boolean.prototype.valueOf.call(target))
    case numberTag:
      return new Object(Number.prototype.valueOf.call(target))
    case stringTag:
      return new Object(String.prototype.valueOf.call(target))
    case symbolTag:
      return new Object(Symbol.prototype.valueOf.call(target))
    case errorTag:
    case dateTag:
      return new Ctor(target)
    case regexpTag:
      return handleRegExp(target)
    case funcTag:
      return handleFunc(target)
    default:
      return new Ctor(target)
  }
}

const deepClone = (target, map = new WeakMap()) => {
  if (!isObject(target)) return target
  let type = getType(target)
  let cloneTarget
  if (!canTraverse[type]) {
    // 处理不能遍历的对象
    return handleNotTraverse(target, type)
  } else {
    // 这波操作相当关键,可以保证对象的原型不丢失!
    let ctor = target.constructor
    cloneTarget = new ctor()
  }

  if (map.get(target)) return target
  map.set(target, true)

  if (type === mapTag) {
    //处理Map
    target.forEach((item, key) => {
      cloneTarget.set(deepClone(key, map), deepClone(item, map))
    })
  }

  if (type === setTag) {
    //处理Set
    target.forEach((item) => {
      cloneTarget.add(deepClone(item, map))
    })
  }

  // 处理数组和对象
  for (let prop in target) {
    if (target.hasOwnProperty(prop)) {
      cloneTarget[prop] = deepClone(target[prop], map)
    }
  }
  return cloneTarget
}