前端手写系列之实现深拷贝

264 阅读11分钟

前言

一个变量的值为基本数据类型时,其值是独立存储的,将该变量复制给另一个变量时,会新生成一个值,赋予另一个变量,因此没有深浅拷贝,或者说都是深拷贝。

对象的存储是由变量存储一个内存地址,该地址存储指向存储着的值。将对象赋予另一变量时,只会把内存地址赋予另一变量,也就是浅拷贝,内存的值是共享的

拷贝:是指通过一定程序将某个变量的值复制至另一个变量的过程。

浅拷贝:只是复制对象最外层的属性也就是赋值了引用地址,导致两个变量仍指向同一内存,一旦值被修改,两边都会产生变化

深拷贝:复制整个对象最外层和深层的所有属性,值被修改互不影响

勉强会工作的前端(简单使用JSON方法)

工作中,我们需要处理的大多为对象中包裹的数据类型都为基本数据类型,也就是数字-布尔值-字符串

const obj1 = {
  a: {
    b: 1, //最终为数字
    c: true, //最终为数字
    d: "1", //最终为数字
  },
};
const obj2 = JSON.parse(JSON.stringify(obj1));

了解JSON方法

JSON方法含有序列化JSON.stringify(将对象序列化为JSON字符串)与反序列化JSON.parse(将JSON字符串转换为新的对象),使用的前提是对象的全部属性都是可序列化的。

JSON.stringify(value[, replacer [, space]])
value:序列化的值
replacer:
	如果该参数是一个函数,则在序列化过程中,被序列化的值的每个属性都会经过该函数的转换和处理;
	replacer(key,val)//第一次的值是整个value,第二次开始才是
	如果该参数是一个数组,则只有包含在这个数组中的属性名才会被序列化到最终的 JSON 字符串中;
	如果该参数为 null 或者未提供,则对象所有的属性都会被序列化。
space:缩进空白字符串,用于美化输出
  • 序列化注意点
    1. undefined:对象中直接忽视,数组中转换为null输出
    2. 不可枚举对象Object.create(null,{x:{value:'x',enumerable:false}})
    3. Symbol和在转换过程中会被直接忽视
    4. 函数在转换过程中会被直接忽视。不过单独转换时JSON.stringify(function(){}) or JSON.stringify(undefined)会被转换为undefined
    5. 包含循环引用的对象会直接报出错误
    6. NaNinfinity格式的数字和null会被转换为null
    7. Date对象会被转换为字符串(同使用了Date.toString()的结果)
    8. 其他对象,包括MapSetWeakMapWeakSet只会序列化自身可枚举对象,默认没有转换为{},同时会失去构造函数

由于拥有以上缺点,最好JSON方法仅适用于已知的只包含基本数据类型**字符串,数组,布尔值**

自实现深拷贝(公共函数中的deepClone)

第一步:实现数据类型判断函数

根据上个的方法,得出,拷贝必须根据不同类型进行不同的处理才能得到正确的值

/**
       * @retrun {isString:fundtion(),...} 返回一个对象,里存放着判断函数
      */
function getIsTpyeofFn() {
  'use strict';
  // 非严格模式下--undefined会执行全window
  const types =
        "Array Object String Date RegExp Function Boolean Number Null Undefined".split(
          " "
        );
  function type() {
    // toString函数会返回数据类型,从索引为8开始截取字符串,能得到数据类型的值
    return Object.prototype.toString.call(this).slice(8, -1);
  }
  const _ = {};
  for (let index = 0; index < types.length; index++) {  
    const cur = types[index];
    _["is" + cur] = (function (self) {
      return function (elem) {
        return type.call(elem) === self;
      };
    })(cur);
  }
  return _;
}

第二步:实现深拷贝函数

核心思想:根据不同类型不同处理,当值为Object或者Array时深入递归处理

/**
 * @return 返回一个新的正则对象
 */
function cloneRegExp(reg) {
  let flag = ''
  if (reg.ignoreCase) {
    flag +='i'
  }
  if (reg.global) {
    flag +='g'
  }
  if (reg.multiline) {
    flag +='im'
  }
  let cloneReg = new RegExp(reg,flag)
  if (reg.lastIndex) {
    cloneReg.lastIndex = reg.lastIndex
  }
  return cloneReg
}


function deepClone(source) {
  const _ = getIsTpyeofFn();
  // 维护两个存储循环引用的数组
  let parents = [];// 将拆分并存储所有对象地址--第一个对象地址存储存放 source本身
  let children = [];// 将拆分并存储所有对象地址--第一个对象地址存储存放 目标本身
  // 用于递归的_clone函数
  function _clone(parent) {
    // 先处理基本数据类型--直接返回
    if (parent === null) {
      return null;
    }
    if (typeof parent !== "object") {
      return parent;
    }
    let child,proto;
    // 下面的都为对象
    if (_.isArray(parent)) {
      // 处理数组对象
      child = []
    } else if (_.isRegExp(parent)) {
      // 处理正则对象
      child = cloneRegExp(parent)
    } else if (_.isDate(parent)) {
      // 处理Date对象
      child = new Date(parent.getTime())
    } else {
      // 处理对象原型
      // 创建一个原型指向parent的新对象
      child = Object.create(Object.getPrototypeOf(parent))
    }
    // 处理循环引用
    if (parents.indexOf(parent) !== -1) {
      // 如果父数组存在本对象,说明之前已经被引用过,直接返回次对象
      return children[index]
    }
    // 没有引用过,则添加至parents和children数组中
    parents.push(parent)
    children.push(child)
    // 遍历对象属性
    for (const prop in parent) {
      if (Object.hasOwnProperty.call(parent, prop)) {
        child[prop] = _clone(parent[prop])
      }
    }
    return child
  }
  return _clone(source)
}

jQurey中的深拷贝解析

jQuery中有两个复制方法,分别是$.clone()$.extends()

其中clone是用于拷贝Dom对象

extends是用于拷贝js对象

extends函数

核心思想,递归变量 对象和数组,遇到基本数据类型直接返回

jQuery.extend = jQuery.fn.extend = function () {
  // options是一个缓存遍历,存储arguments[i]
  // name是用来接收要被扩展对象的key,src改变之前target对象上每个key对应的value
  // copy传入对象上每个key对应的value,copyIsArray判断是否为一个数组
  // clone深克隆中用来临时存储对象或者数组的src
  var src, copyIsArray, copy, name, options, clone;
  (target = arguments[0] || {}), (i = 1), (length = arguments.length);
  deep = false;

  // 如果传递的第一个参数为boolean类型,为true代表深拷贝,为false代表浅拷贝
  if (typeof target === "boolean") {
    deep = target;
    // 如果传递了第一个参数为boolean值,则待克隆的对象为第二个参数
    target = arguments[i] || {};
    i++;
  }
  // 如果是基本数据类型
  if (typeof target !== "object" && !jQuery.isFunction(target)) {
    target = {};
  }
  // 如果只传递一个参数,那么克隆的是jquery自身
  if (i === length) {
    target = this;
    i--;
  }
  for (; i < length; i++) {
    // 仅需要处理不是null 与undefined类型的数据
    if(options = arguments[i] !=null){
      //遍历对象的所有属性
      for (const name in options) {
        src = target[name]
        copy = options[name]
        // 阻止循环引用
        if(target ===copy){
          continue
        }
        // 递归处理对象和数值
        if (deep&&copy&&(jQuery.isPlainObject(copy))||(copyIsArray = jQuery.isArray(copy))) {
          if (copyIsArray) {
            copyIsArray = false;
            clone = src && jQuery.isArray(src)?src :[]
          }else{
            clone = src && jQuery.isPlainObject(src)?src:{}
          }
          // 将原始值的name属性赋值给target目标对象
          target[name] = jQuery.extend(deep,clone,copy)
        }else{
          //简单值,直接赋值
          target[name] =  copy
        }
      }
    }
  }
  return target
};

extends函数有一个缺陷就是不能拷贝重复引用

lodash工具之深拷贝函数解析

_.cloneDeep(value)这个方法类似_.clone,除了它会递归拷贝 value。(注:也叫深拷贝)。

二话不多说贴上源码

cloneDeep方法

/**
 * This method is like `_.clone` except that it recursively clones `value`.
 *
 * @static
 * @memberOf _
 * @since 1.0.0
 * @category Lang
 * @param {*} value The value to recursively clone.
 * @returns {*} Returns the deep cloned value.
 * @see _.clone
 * @example
 *
 * var objects = [{ 'a': 1 }, { 'b': 2 }];
 *
 * var deep = _.cloneDeep(objects);
 * console.log(deep[0] === objects[0]);
 * // => false
 */
function cloneDeep(value) {
  return baseClone(value, CLONE_DEEP_FLAG | CLONE_SYMBOLS_FLAG);
}

这个方法类似_.clone,除此之外还递归的拷贝了value。

一个参数:value(需要深拷贝的值);会直接返回一个深拷贝的对象

baseClone方法

cloneDeep里边会调用baseClone方法同时把value传入

/** Used to compose bitmasks for cloning. */
/** 组成位掩码用于拷贝 */
/** cloneDeep中CLONE_DEEP_FLAG | CLONE_SYMBOLS_FLAG=> 1 | 4  = 5 */
var CLONE_DEEP_FLAG = 1,
    CLONE_SYMBOLS_FLAG = 4;
/**
 * The base implementation of `_.clone` and `_.cloneDeep` which tracks
 * traversed objects.
 *
 * @private
 * @param {*} value The value to clone.
 * @param {boolean} bitmask The bitmask flags.
 *  1 - Deep clone 为1时深拷贝
 *  2 - Flatten inherited properties 为2时打平集成属性
 *  4 - Clone symbols 为4时复制symbols类型数据
 * @param {Function} [customizer] The function to customize cloning. 
 * @param {Function} [customizer] 自定义clone函数. 
 * @param {string} [key] The key of `value`.
 * @param {Object} [object] The parent object of `value`.
 * @param {Object} [stack] 跟踪遍历对象,存储他们的副本.
 * @returns {*} Returns the cloned value.
 */
function baseClone(value, bitmask, customizer, key, object, stack) {
  var result,
      isDeep = bitmask & CLONE_DEEP_FLAG, // 5 & 1 = 1 (位与运算)深拷贝
      isFlat = bitmask & CLONE_FLAT_FLAG,  // 5 & 2 = 0 (位与运算)不进行原型复制
      isFull = bitmask & CLONE_SYMBOLS_FLAG; // 5 & 4  = 4 (位与运算)对symbol进行拷贝

  // customizer 自定义克隆,并返回函数的返回值
  if (customizer) {
    result = object ? customizer(value, key, object, stack) : customizer(value);
  }
  if (result !== undefined) {
    return result;
  }

  //基础数据类型直接返回
  if (!isObject(value)) {
    return value;
  }
  // 由于基本数据类型都被上面原地返回,下面处理都是对象类型的值

  // 数组处理
  var isArr = isArray(value);
  if (isArr) {
    // 初始化数组,使其结构上类似原数组
    result = initCloneArray(value);
    if (!isDeep) {
      // 不进行深拷贝的直接返回浅拷贝值
      return copyArray(value, result);
    }
  } 
  // 对非数组处理
  else {
    var tag = getTag(value),
        isFunc = tag == funcTag || tag == genTag;
    // tag 返回类型文本 '[object Symbol]'
    // isFunc 是否为函数
    
    if (isBuffer(value)) {
      // 对于二进制流对象克隆后直接返回不进行递归
      return cloneBuffer(value, isDeep);
    }

      // 处理普通对象类型跟类似数组类型还有 单纯的函数对象
    if (tag == objectTag || tag == argsTag || (isFunc && !object)) {
      // 对于函数,没有父对象时不进行拷贝直接返回空对象
      // 原型链复制
      result = (isFlat || isFunc) ? {} : initCloneObject(value);
      if (!isDeep) {
        return isFlat
          ? copySymbolsIn(value, baseAssignIn(result, value))
          : copySymbols(value, baseAssign(result, value));
      }
    } else {
      if (!cloneableTags[tag]) {
        // 是否为可拷贝对象--错误对象,函数对象,weakMap对象-没有父对象时,返回空对象,有父对象则返回原值
        return object ? value : {};
      }
      // 对于非常规类型对象,通过各自类型分别进行处理。
      result = initCloneByTag(value, tag, isDeep);
    }
  }
  // 用栈将引用存储起来
  stack || (stack = new Stack);
  var stacked = stack.get(value);
  if (stacked) {
    // 发现循环引用,直接返回循环引用
    return stacked;
  }
  stack.set(value, result);
  // set数据处理--遍历递归
  if (isSet(value)) {
    value.forEach(function(subValue) {
      result.add(baseClone(subValue, bitmask, customizer, subValue, value, stack));
    });
  } else if (isMap(value)) {
  // map数据处理--遍历递归
    value.forEach(function(subValue, key) {
      result.set(key, baseClone(subValue, bitmask, customizer, key, value, stack));
    });
  }

  // 此处得到getAllKeys
  var keysFunc = isFull
    ? (isFlat ? getAllKeysIn : getAllKeys)
    : (isFlat ? keysIn : keys);

  // 获取拷贝对象的所有属性键-遍历并递归--然后对result进行赋值
  var props = isArr ? undefined : keysFunc(value);
  arrayEach(props || value, function(subValue, key) {
    if (props) {
      key = subValue;
      subValue = value[key];
    }
    // Recursively populate clone (susceptible to call stack limits).
    assignValue(result, key, baseClone(subValue, bitmask, customizer, key, value, stack));
  });
  // 返回深拷贝的result
  return result;
}

核心原理

深度拷贝:对于普通对象跟数组,使用baseclone函数进行遍历递归,递归的结束条件是得到一个拷贝后的对象或者基本数据类型

// 获取拷贝对象的所有属性键-遍历并递归--然后对result进行赋值
var props = isArr ? undefined : keysFunc(value);
arrayEach(props || value, function(subValue, key) {
  if (props) {
    key = subValue;
    subValue = value[key];
  }
  // Recursively populate clone (susceptible to call stack limits).
  assignValue(result, key, baseClone(subValue, bitmask, customizer, key, value, stack));
});

循环引用:创建一个栈。将每个引用类型都存入栈中,返现被拷贝对象有循环引用时,返回拷贝后的对象,组程新的循环引用

// 用栈将引用存储起来
stack || (stack = new Stack);
var stacked = stack.get(value);
if (stacked) {
  // 发现循环引用,直接返回循环引用
  return stacked;
}

基本数据类型处理:统一判断为非对象,直接返回原值。基本数据类型的值每一个都是互不影响的

//基础数据类型直接返回
if (!isObject(value)) {
  return value;
}

数组跟RegExp.exce()返回的数组:使用initCloneArray生成与拷贝数组一致的数组结构result,用于保存遍历的结果

// 数组处理
var isArr = isArray(value);
if (isArr) {
  // 初始化数组,使其结构上类似原数组
  result = initCloneArray(value);
  ...
} 
function initCloneArray(array) {
  var length = array.length,
      result = new array.constructor(length);

  // Add properties assigned by `RegExp#exec`.
  // `RegExp#exec`的判断逻辑,长度大于0,索引为1的值是字符串且拥有index属性
  if (length && typeof array[0] == 'string' && hasOwnProperty.call(array, 'index')) {
    result.index = array.index;
    result.input = array.input;
  }
  return result;
}

对象和函数:二进制对象,返回拷贝对象,不进行递归,错误对象,weakmap弱引用对象,函数存在父对象时,返回原函数,否则返回空对象。 其他对象类型,根据匹配出的类型,初始化为对应的对象类型

var tag = getTag(value),
    isFunc = tag == funcTag || tag == genTag;
// tag 返回类型文本 '[object Symbol]'
// isFunc 是否为函数

if (isBuffer(value)) {
  // 对于二进制流对象克隆后直接返回不进行递归
  return cloneBuffer(value, isDeep);
}

  // 处理普通对象类型跟类似数组类型还有 单纯的函数对象
if (tag == objectTag || tag == argsTag || (isFunc && !object)) {
  // 对于函数,没有父对象时不进行拷贝直接返回空对象
  // 原型链复制
  result = (isFlat || isFunc) ? {} : initCloneObject(value);
  if (!isDeep) {
    return isFlat
      ? copySymbolsIn(value, baseAssignIn(result, value))
      : copySymbols(value, baseAssign(result, value));
  }
} else {
  if (!cloneableTags[tag]) {
    // 是否为可拷贝对象--错误对象,函数对象,weakMap对象-没有父对象时,返回空对象,有父对象则返回原值
    return object ? value : {};
  }
  // 对于非常规类型对象,通过各自类型分别进处理。
  result = initCloneByTag(value, tag, isDeep);
}

function initCloneByTag(object, tag, isDeep) {
  // 保存原型对象,使得拷贝后仍保持原型链(通过new 构造函数生成对象)
  var Ctor = object.constructor;
  switch (tag) {
    case arrayBufferTag:
      return cloneArrayBuffer(object);

    case boolTag:
    case dateTag:
      return new Ctor(+object);

    case dataViewTag:
      return cloneDataView(object, isDeep);

    case float32Tag: case float64Tag:
    case int8Tag: case int16Tag: case int32Tag:
    case uint8Tag: case uint8ClampedTag: case uint16Tag: case uint32Tag:
      return cloneTypedArray(object, isDeep);

    case mapTag:
      return new Ctor;

    case numberTag:
    case stringTag:
      return new Ctor(object);

    case regexpTag:
      return cloneRegExp(object);

    case setTag:
      return new Ctor;

    case symbolTag:
      return cloneSymbol(object);
  }
}

正则类型:巧用正则的转换为字符后,固定类型,获取正则对象的标志参数

// 适用于获取以字母结尾的字符串,这些字符串也就是正则的标志参数
const reFlags = /\w*$/ 
function cloneRegExp(regexp) {
  // 返回当前匹配的文本
  const result = new regexp.constructor(regexp.source, reFlags.exec(regexp))
  // 下一次匹配的起始索引
  result.lastIndex = regexp.lastIndex
  return result
}

symbol类型:

const symbolValueOf = Symbol.prototype.valueOf
function cloneSymbol(symbol) {
  return symbolValueOf ? Object(symbolValueOf.call(symbol)) : {};
}

对于map和set数据类型:使用forEach函数遍历,再使用baseClone进行递归

if (tag == mapTag) {
    value.forEach((subValue, key) => {
        result.set(key, baseClone(subValue, bitmask, customizer, key, value, stack))
    })
    return result
}
if (tag == setTag) {
    value.forEach((subValue) => {
        result.add(baseClone(subValue, bitmask, customizer, subValue, value, stack))
    })
    return result
}

参考:木易杨大佬的文章Lodash是如何实现深拷贝的

性能对比

测试代码

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script
      type="text/javascript"
      src="https://cdn.jsdelivr.net/g/lodash@4(lodash.min.js+lodash.fp.min.js)"
    ></script>
    <script src="https://cdn.bootcss.com/jquery/1.12.4/jquery.min.js"></script>

    <script>
      const lodashClone = _.cloneDeep;
      const jqueryClone = jQuery.extend;
      /**
       * @return 返回一个新的正则对象
       */
      function cloneRegExp(reg) {
        let flag = "";
        if (reg.ignoreCase) {
          flag += "i";
        }
        if (reg.global) {
          flag += "g";
        }
        if (reg.multiline) {
          flag += "im";
        }
        let cloneReg = new RegExp(reg, flag);
        if (reg.lastIndex) {
          cloneReg.lastIndex = reg.lastIndex;
        }
        return cloneReg;
      }

      function deepClone(source) {
        const _ = getIsTpyeofFn();
        // 维护两个存储循环引用的数组
        let parents = []; // 将拆分并存储所有对象地址--第一个对象地址存储存放 source本身
        let children = []; // 将拆分并存储所有对象地址--第一个对象地址存储存放 目标本身
        // 用于递归的_clone函数
        function _clone(parent) {
          // 先处理基本数据类型--直接返回
          if (parent === null) {
            return null;
          }
          if (typeof parent !== "object") {
            return parent;
          }
          let child, proto;
          // 下面的都为对象
          if (_.isArray(parent)) {
            // 处理数组对象
            child = [];
          } else if (_.isRegExp(parent)) {
            // 处理正则对象
            child = cloneRegExp(parent);
          } else if (_.isDate(parent)) {
            // 处理Date对象
            child = new Date(parent.getTime());
          } else {
            // 处理对象原型
            // 创建一个原型指向parent的新对象
            child = Object.create(Object.getPrototypeOf(parent));
          }
          // 处理循环引用
          if (parents.indexOf(parent) !== -1) {
            // 如果父数组存在本对象,说明之前已经被引用过,直接返回次对象
            return children[index];
          }
          // 没有引用过,则添加至parents和children数组中
          parents.push(parent);
          children.push(child);
          // 遍历对象属性
          for (const prop in parent) {
            if (Object.hasOwnProperty.call(parent, prop)) {
              child[prop] = _clone(parent[prop]);
            }
          }
          return child;
        }
        return _clone(source);
      }
      /**
       * @retrun {isString:fundtion(),...} 返回一个对象,里存放着判断函数
       */
      function getIsTpyeofFn() {
        "use strict";
        // 非严格模式下--undefined会执行全window
        const types =
          "Array Object String Date RegExp Function Boolean Number Null Undefined".split(
            " "
          );
        function type() {
          // toString函数会返回数据类型,从索引为8开始截取字符串,能得到数据类型的值
          return Object.prototype.toString.call(this).slice(8, -1);
        }
        const _ = {};
        for (let index = 0; index < types.length; index++) {
          const cur = types[index];
          _["is" + cur] = (function (self) {
            return function (elem) {
              return type.call(elem) === self;
            };
          })(cur);
        }
        return _;
      }
      var objects = [{ a: 1 }, { b: 2 }];
      function test(n) {
        console.time(`deepClone拷贝${n}次,使用时间为`);
        for (let index = 0; index < n; index++) {
          deepClone(objects);
        }
        console.timeEnd(`deepClone拷贝${n}次,使用时间为`);

        console.time(`lodashClone拷贝${n}次,使用时间为`);
        for (let index = 0; index < n; index++) {
          lodashClone(objects);
        }
        console.timeEnd(`lodashClone拷贝${n}次,使用时间为`);

        console.time(`jqueryClone拷贝${n}次,使用时间为`);
        for (let index = 0; index < n; index++) {
          jqueryClone(objects);
        }
        console.timeEnd(`jqueryClone拷贝${n}次,使用时间为`);
      }
      test(100);
      test(1000);
      test(10000);
      test(100000);
      test(1000000);
    </script>
  </body>
</html>

测试结果

image.png

测试次数/时间(ms)lodashClonejqueryClonedeepClone
百次1.150.360.14
千次6.783.685.80
万次19.1943.3827.43
十万次136.45344.67265.28
百万次1352.853507.432592.95

结果分析

百次以内,自实现用时最少,因为针对业务,逻辑也比较少。

千次以内,Jq实现用时最少,lodash用时最长。

万次以上,Jq实现用时最长,lodash用时最短,如果自实现的话,还可以针对业务对自实现深拷贝根据lodash源码进行优化。

业务使用

平时我们用的深拷贝多数用于,普通对象以及数组,跟正则,可以使用lodash的clonedeep方法进行项目开发。

最优解是,针对业务简写clonedeep方法,生成统一的工具函数deepclone