最全的纯手撕深浅合并和深浅拷贝

120 阅读7分钟

写在前面,如果下面有的封装的函数没有找到,请进入我通用 工具集锦 文章查找对应函数方法

// 类型检测方法
const toType = function toType(obj) {
  const reg = /^\[object ([\w\W]+)\]$/;
  if (obj === null) return obj + "";
  // const isObj = typeof obj === 'object' || typeof obj === 'function';
  const isObj = /^(object|fuction)$/.test(typeof obj),
    getObjClass = Object.prototype.toString;
  // reg.exec(getObjClass.call(obj)) 返回的是一个数组,数组第二项[1]表示 第一个括号内的匹配结果
  return isObj ? reg.exec(getObjClass.call(obj))[1].toLowerCase(): typeof obj
}

深浅合并

let obj1 = {
    name: '张三',
    age: 25,
    hobby: {
        music: 100,
        jump: 80
    }
};
// obj1.obj1 = obj1;
let obj2 = {
    name: '李四',
    age: 22,
    sex: 0,
    hobby: {
        read: 100,
        music: 90
    }
};
let obj3 = {
    name: '王五',
    age: 20,
    height: '158cm',
    score: {
        math: 100,
        chinese: 90
    }
};
let obj = Object.assign(obj1,obj2);
console.log(obj,obj === obj1, obj2); // 浅合并,obj === obj1为 true

不带套娃的数组或者对象的深浅合并

思路:首先进行参数分解,第一个参数如果是个boolean 类型,说明控制深浅合并的,target目标项也就是被替换项,options是之后的每一个替换项,length用来循环迭代的,i=1,表示从第二项开始,往后累加。

  • 如果第一项是boolean ,说明本意想进行深合并的标识符,则第二项是被替换项(params[i]),也就是target,此时循环迭代从 ++i 开始,也就是i=2,第三项开始。
    • 首先参数条件判断,如果options 有一项是空,直接进入下一次循环
    • target 必须的是一个对象,不是默认空对象
    • 循环迭代,每一项中,如果替换项是数组,先判断被替换项是不是数组,不是数组,默认置为空数组
    • 同理替换项是对象的话,先判断被替换项是不是对象,不是对象默认置为空对象
  • 如果第一项不是 boolean ,则默认浅合并,第一项是target,此时循环迭代从第二项开始,也就是 i = 1这一项
    • 如果替换项存在,则直接替换
// 实现数组&对象的“深浅”合并
const merge = function merge(...params) {
  let options, // 后面的替换项
    target = params[0] || {}, // 最前面的被替换项
    i = 1,
    length = params.length,
    deep = false; // 浅合并
  // 第一项事布尔值:传递给 deep 控制深浅合并的
  if (typeof target === 'boolean') {
    deep = target;
    target = params[i] || {};
    i++
  }
  // 必须保证被替换项是一个对象
  if (target == null || (typeof target !== 'object' && !isFunction(target))) target = {};
  // 迭代传递的剩下的对象,一次替换target
  for (; i < length; i++){
    options = params[i]; // 获取某一替换项
    if (options == null) continue;
    // 一次拿出替换项中每一项,替换“被替换项”中同名这一项
    each(options, (copy, name) => {
      // name 是键名,copy 是键值——替换项
      let copyIsArray = Array.isArray(copy),
        copyIsObject = isPlainObject(copy),
        src = target[name]; // 被替换项
      if (deep && copy && (copyIsArray || copyIsObject)) {
        // 深合并
        if (copyIsArray && !Array.isArray(src)) src = []
        if (copyIsObject && !isPlainObject(src)) src = {};
        target[name] = merge(deep, src, copy); // 递归
      } else if (copy !== undefined) {
        // 浅合并
        target[name] =copy
      }
    })
  }
  // 返回被替换这一项
  return target
}

带套娃的深浅合并

思路:思路大致同不带套娃的深浅合并类似,就是多了 treated 参数,他是一个 Set 集合,里面有个 isHandler 记录被处理过的数据,如果有,则直接返回不处理了,目的防止类似 obj.obj = obj这种套娃引起的死循环。

  • 第一次进行深浅拷贝,所以treated 中没有 isHandler,之后进行处理,加入到 Set 集合中,此时 treated 有 ishandler,下一次进行处理前,先从 Set 集合中先判断该数据有没有被处理过,若处理过,直接返回,没有处理,则进行循环处理,这里注意,进行迭代调用merge 时, treated 需要作为参数进行迭代
  • 之后当 treated 类型为 set 数据类型,则此时参数多了一个,所以 params 的length 就需要 --length
// 实现数组&对象的“深浅”合并
const merge = function merge(...params) {
  let options, // 后面的替换项
    target = params[0] || {}, // 最前面的被替换项
    i = 1,
    length = params.length,
    deep = false, // 浅合并
    /*套娃 start*/
    treated = params[length - 1];
  // 最开始没有treated 或者递归的时候有了
  if (ToType(treated) === 'set' && treated.isHandler) {
    // 递归执行merge的时候
    length--;
  } else {
    // 第一次执行 merge
    treated = new Set();
    treated.isHandler = true
  }
  /*套娃 end */
  // 第一项事布尔值:传递给 deep 控制深浅合并的
  if (typeof target === 'boolean') {
    deep = target;
    target = params[i] || {};
    i++
  }
  // 必须保证被替换项是一个对象
  if (target == null || (typeof target !== 'object' && !isFunction(target))) target = {};
  // 迭代传递的剩下的对象,一次替换target
  for (; i < length; i++){
    options = params[i]; // 获取某一替换项
    if (options == null) continue;
    // 防止死递归的处理
    if (treated.has(options)) return options;
    treated.add(options);
    // 依次拿出替换项中每一项,替换“被替换项”中同名这一项
    each(options, (copy, name) => {
      // name 是键名,copy 是键值——替换项
      let copyIsArray = Array.isArray(copy),
        copyIsObject = isPlainObject(copy),
        src = target[name]; // 被替换项
      if (deep && copy && (copyIsArray || copyIsObject)) {
        // 深合并
        if (copyIsArray && !Array.isArray(src)) src = []
        if (copyIsObject && !isPlainObject(src)) src = {};
        // target[name] = merge(deep, src, copy);
        /*套娃 start*/ // 多了最后一项 treated
        target[name] = merge(deep, src, copy,treated); // 递归
        /*套娃 end */
      } else if (copy !== undefined) {
        // 浅合并
        target[name] =copy
      }
    })
  }
  // 返回被替换这一项
  return target
}

深浅拷贝

只拷贝第一层,第二层及以后都是相同地址

不带套娃的拷贝

思路:深浅拷贝中,首先参数处理,有一个参数,两个参数这两种情况,只有一个参数,说明想进行浅拷贝,有两个参数,且第一个参数是 Boolean 值,说明想进行深拷贝。

target,被拷贝的值,deep,标志位,判断是否深浅拷贝,type 被克隆值的类型,result 克隆的结果

  • 先判断第一个参数是否是 boolean 值,如果是 boolean 值,且有第二个参数,则说明进行深拷贝,此时 被拷贝的项将从第二项开始,也就是 i=1 的时候
  • 判断 target 的类型,如果不是数组,不是普通对象,不是函数,则typeof 类型检测为 object ,则就是标准特殊对象,和非标准特殊对象。这时创建一个相同类型的数据类型值还是该值,new target.constructor(target) 即可。
  • 然后不是数组也不是对象,则是基本数据类型
  • 如果是数组和普通对象,且进行深拷贝的时候,先创建空数组或者空对象,然后则进行循环迭代,结果放入先创建的数组或对象中。
// 实现深浅拷贝
const clone = function clone(...params) {
  // parmas 一般也都两项,target 被拷贝得值,deep 深浅合并选项
  let target = params[0],
    length = params.length,
    deep = false,
    type,// 克隆值得类型
    isArray, // 检测克隆值是否为数组,或对象
    isObject,
    result;
  // 第一项是 boolean ,且有第二项值
  if (typeof target === 'boolean' && length >1) {
    deep = target;
    target = params[1]
  }
  type = toType(target);
  isArray = Array.isArray(target);
  isObject = isPlainObject(target);
  // 特殊值得拷贝【函数无需拷贝】
  // null 和 undefined
  if (target == null) return target;
  if (!isArray && !isObject && !isFunction(target) && typeof target === 'object') {
    // 标准特殊对象和 非标准特殊对象
    // 实例的constructor,所属类原型赏的 constructor,当前构造函数本身
    try {
      return new target.constructor(target);
    } catch (error) {
      return target
    }
  }
  // 原始值
  if (!isArray && !isObject) return target;
  // 数组和对象
  result = new target.constructor();
  each(target, (copy, name) => {
    if (deep) {
      // 深拷贝
      result[name] = clone(deep, target)
      return;
    }
    // 浅拷贝
    result[name] = copy;
  })
  return result
}

带套娃的拷贝

思路: 这里和不带套娃深浅拷贝类似,只是多了一个标志位 treated ,刚开始treated 为空。刚开始第一个进行拷贝函数时,treated 为空,此时将 treated 赋值为一个空的 Set 集合,之后每处理一个数据,都将添加到 treated 中,之后每次处理数据,先从treated 中判断是否有被处理过,被处理过了则直接返回数据。

循环迭代时,注意参数传递多了一个 treated

// 实现深浅拷贝
const clone = function clone(...params) {
  // parmas 一般也都两项,target 被拷贝的值,deep 深浅合并选项
  let target = params[0],
    length = params.length, // 注意参数个数,不多
    deep = false,
    i=1,
    type,// 克隆值得类型
    isArray, // 检测克隆值是否为数组,或对象
    isObject,
    treated, // set集合,记录已经处理过的内容
    result;
  // 第一项是 boolean ,且有第二项值
  if (typeof target === 'boolean' && length >1) {
    deep = target;
    target = params[1];
    i = 2
  }
  // 防止死递归处理
  treated = params[i];
  if (!treated) treated = new Set();
  if (treated.has(target)) return target;
  treated.add(target)

  type = toType(target);
  isArray = Array.isArray(target);
  isObject = isPlainObject(target);
  // 特殊值得拷贝【函数无需拷贝】
  // null 和 undefined
  if (target == null) return target;
  if (!isArray && !isObject && !isFunction(target) && typeof target === 'object') {
    // 标准特殊对象和 非标准特殊对象
    // 实例的constructor,所属类原型赏的 constructor,当前构造函数本身
    try {
      return new target.constructor(target);
    } catch (error) {
      return target
    }
  }
  // 原始值
  if (!isArray && !isObject) return target;
  // 数组和对象
  result = new target.constructor();
  each(target, (copy, name) => {
    if (deep) {
      // 深拷贝
      result[name] = clone(deep, target,treated)
      return;
    }
    // 浅拷贝
    result[name] = copy;
  })
  return result
}