js中的几种浅拷贝方式,以及如何手写深拷贝

86 阅读4分钟

浅拷贝

对象

var obj = {a:1,b:[1,2]}
var copyObj = Object.assign(obj,{}) // assign合并
var copyObj = {...obj} //es6的展开运算符合并

数组

var arr = [1,2]
slice()
var copyArr = arr.concat([]) // concat合并
var copyArr = [...arr] //es6的展开运算符合并
var copyArr = arr.slice() // slice切割

优点: 处理1层数据的时候能简单便捷地完成“深”拷贝。 缺点: 由于只是内存地址的拷贝,当对其下层的数组及对象进行操作时还是会改变原始数据。

console.log(obj);//{a:1,b:[1,2]}
copyObj.b.push(3);
console.log(obj);//原对象值被修改为{a:1,b:[1,2,3]}

深拷贝

方法一: JSON的序列号以及反序列化

let obj = {};
let copyObj = JSON.pase(JSON.stringify(obj));

优点: 可以快速地对多层简单数据进行深拷贝。

缺点: 部分数据会存在丢失以及功能性损坏, 如日期在转换后会变成纯字符串,正则在转换后变成空对象,undefined、函数、symbol等字段丢失,NaN、Infinity和-Infinity 变成null,对象内循环引用时报错

let obj = {
    a:'正常情况',
    '时间':new Date(1536627600000),
    '正则':new RegExp('[0-9]'),
    'undefined':undefined,
    'NaN等':NaN,
    '函数':function(){}
};
let copyObj = JSON.pase(JSON.stringify(obj));

console.log(copyObj);
/*
{
    'NaN等': null
    a: "正常情况"
    '时间': "2018-09-11T01:00:00.000Z"
    '正则': {}
}
*/

方法二:递归克隆

为解决方法1中的问题,我们采用自定义函数对数据进行递归以及特殊数据判断以达到深拷贝目的。本文采用总分总的形式分析如何实现,具体思路为:

  1. 对将要克隆的对象/值的类型汇总分析。
  2. 根据分析出的各种类型一一写出该类型对应的克隆方法。
  3. 将不同的情况整合在一起构成我们需要的函数。
类型汇总分析
引用类型判断

该判断主要区分是否为引用类型,如果是非引用类型,则直接返回值既可完成克隆。

function isObject(target) {
    const type = typeof target;
    return target !== null && (type === 'object' || type === 'function');
}
引用类型分类

getType 函数将通过调用 Object 原型上的 toString( )方法 判断传入对象的准确引用类型

function getType(target) {
    return Object.prototype.toString.call(target);
}

以下是常见类型对应 getType 后的值

const mapTag = '[object Map]';
const setTag = '[object Set]';
const arrayTag = '[object Array]';
const objectTag = '[object Object]';

const deepTag = [mapTag,setTag,arrayTag,objectTag];

const funcTag = '[object Function]';
const boolTag = '[object Boolean]';
const dateTag = '[object Date]';
const errorTag = '[object Error]';
const numberTag = '[object Number]';
const regexpTag = '[object RegExp]';
const stringTag = '[object String]';
const symbolTag = '[object Symbol]';

在上面的集中类型中,我们简单将他们分为两类:

  • 可以继续遍历的类型 (deepTag)
  • 不可以继续遍历的类型
各种类型克隆方法
非引用类型
function clone (target){
    //非引用类型
    if (!isObject(target)) {
        return target;
    }
    ... // 其它情况
}
可遍历引用类型

首先我们要解决循环引用问题

我们可以额外开辟一个存储空间,来存储当前对象和拷贝对象的对应关系,当需要拷贝当前对象时,先去存储空间中找,有没有拷贝过这个对象,如果有的话直接返回,如果没有的话继续拷贝,这样就巧妙化解的循环引用的问题。

这个存储空间,需要可以存储key-value形式的数据,且key可以是一个引用类型,我们可以选择Map这种数据结构

function clone (target,map = new Map()){
      ... // 其它情况
      
      // 定义所需参数
      const type = getType(target);
      let cloneTarget;
      
      // 判断是否可以继续遍历
      if (deepTag.includes(type)) {
      /* 保留对象原型上的数据,若使用 cloneTarget = [] || {} 则会丢失原型
      */
        cloneTarget = new target.constructor
      }
     
      // 检查是否已经拷贝过,有的话直接返回防止循环引用
      if (map.has(target)) {
        return map.get(target);
      }
      map.set(target, cloneTarget);
}

克隆Map

function clone (target,map = new Map()){
    //...前面部分参考上文所写
    
    //Map的克隆
    if(type === mapTag){
        target.forEach((value, key) => {
            cloneTarget.set(key, clone(value,map));
        });
        return cloneTarget;
    }
}

克隆Set

function clone (target,map = new Map()){
    //...前面部分参考上文所写
    
    //Set的克隆
    if(type === setTag){
        target.forEach(value => {
            cloneTarget.add(clone(value,map));
        });
        return cloneTarget;
    }
}

克隆 Array 和 Object

function clone (target,map = new Map()){
  //...前面部分参考上文所写
  // 克隆数组和对象 
  const keys = Object.keys(target);
  let index = -1;
  const length = keys.length;
  while (++index < length) {
      const key = keys[index];
      cloneTarget[key] = clone(target[key], map);
  }
  return cloneTarget;
}
不可遍历引用类型

正则的克隆

function cloneReg (target) {
  const reFlags = /\w*$/;
  const result = new target.constructor(target.source, reFlags.exec(target));
  result.lastIndex = target.lastIndex;
  return result;
}

Symbol的克隆

function cloneSymbol (target) {
  return Object(Symbol.prototype.valueOf.call(target));
}

函数的克隆

简单点可以直接返回target。 其它方法:可以通过prototype来区分下箭头函数和普通函数,箭头函数可用 eval 方法克隆 ,普通函数 可以用 new Function 构造新函数 。

function cloneFunction(func) {
    //方法1直接 return func; 
    //方法2 
    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);
    }
}

其它引用的克隆

function cloneOther (target) {
  const Ctor = target.constructor;
  return new Ctor(target)
}

不可遍历克隆的整合

function clone (target,map = new Map()){
      ... // 其它情况
      // 判断是否可以继续遍历
      if (deepTag.includes(type)) {
        cloneTarget = new target.constructor
      } else {
        //新增不可遍历分支
        return cloneOtherType(target, type);
      }
      ... // 其它情况
}

function cloneOtherType (target, type) {
  switch (type) {
    case boolTag:
    case numberTag:
    case stringTag:
    case errorTag:
    case dateTag:
      new cloneOther(target);
    case regexpTag:
      return cloneReg(target);
    case symbolTag:
      return cloneSymbol(target);
    case funcTag:
      return cloneFunction(target);
    default:
      return null;
  }
}
WeakMap 替换 Map

WeakMap 的键为弱引用

弱引用,是指不能确保其引用的对象不会被垃圾回收器回收的引用。 一个对象若只被弱引用所引用,则是不可访问的,因此可以在任何时刻被回收。

function clone (target,map = new WeakMap()){
    ...//克隆逻辑
}
整合

函数主体:

function clone (target,map = new WeakMap()){

      //非引用类型
      if (!isObject(target)) {
        return target;
      }
      
      // 判断是否可以继续遍历
      const type = getType(target);
      
      if (!deepTag.includes(type)) {
        //不可遍历克隆
        return cloneOtherType(target, type);
      } 
      
      /*
      * 可遍历克隆
      */
      let cloneTarget = new target.constructor
     
      // 检查是否已经拷贝过,有的话直接返回防止循环引用
      if (map.has(target)) {
        return map.get(target);
      }
      map.set(target, cloneTarget);
      
    //Map的克隆
    if(type === mapTag){
        target.forEach((value, key) => {
            cloneTarget.set(key, clone(value,map));
        });
        return cloneTarget;
    }
    
    //Set的克隆
    if(type === setTag){
        target.forEach(value => {
            cloneTarget.add(clone(value,map));
        });
        return cloneTarget;
    }
    
    // 数组和对象的克隆
    const keys = Object.keys(target);
    let index = -1;
    const length = keys.length;
    while (++index < length) {
        const key = keys[index];
        cloneTarget[key] = clone(target[key], map);
    }
    return cloneTarget;
}

方法三:第三方库

使用第三方的工具函数,如 lodash 中的 cloneDeep 方法等