lodash 源码解析 -- cloneDeep

2,965 阅读4分钟

lodash 代码版本 4.7.20

函数作用

cloneDeep 作用是将变量数据中所有的值,都依次拷贝一份新的出来,包括但不限于 arraysarray buffersbooleansDatemapsnumbersObjectregexessetsstringssymbolstyped arrays。注意只会拷贝对象的可枚举属性。如果对象不可拷贝,比如是 ErrorFunctionDOMWeakMap,那么返回空对象。和 clone 函数有所不同,clone 只拷贝一层,cloneDeep 会递归拷贝到对象下一层直到不能递归为止

思路分析

cloneDeep 需要关注的几个问题:类型判断、循环引用,内置类型可以用调用 Object.prototype.toString.call 这个方法来判断类型,自定义类型的拷贝可以使用 new constructor 的方式拷贝

源码分析

cloneDeep

首先打开 cloneDeep 函数的文件发现,其底层是由 baseClone 实现的, 实际上 clonecloneDeepclon eDeepWith 等函数底层都是由 baseClone 实现的,lodash 通过传参的方式控制最终的效果

function cloneDeep(value) {
  // 实际上 clone、cloneDeep、cloneDeepWith 等底层都是由 baseClone 实现的,lodash 通过传入 Symbol 的方式控制是否递归
  return baseClone(value, CLONE_DEEP_FLAG | CLONE_SYMBOLS_FLAG);
}

baseClone

baseClone 包含深拷贝和浅拷贝,这里只分析深拷贝

/**
     * @private
     * @param {*} 传入一个任意值
     * @param {boolean} bitmask 符号位,clone 的方式
     *  1 - 深 clone
     *  2 - 抹平继承属性
     *  4 - clone 用途
     * @param {Function} [customizer] 自定义 clone 函数
     * @param {string} [key] value 的所有 key 值
     * @param {Object} [object] value 的父对象
     * @param {Object} [stack] 存储被拷贝和拷贝对象的递归值,同时可以解决循环递归的问题
     * @returns {*} 返回一个拷贝值
 */
function baseClone(value, bitmask, customizer, key, object, stack) {
  var result,
      isDeep = bitmask & CLONE_DEEP_FLAG,
      isFlat = bitmask & CLONE_FLAT_FLAG,
      isFull = bitmask & CLONE_SYMBOLS_FLAG;
  // cloneDeepWith 需要的代码,可以自定义 clone 函数
  if (customizer) {
    result = object ? customizer(value, key, object, stack) : customizer(value);
  }
  if (result !== undefined) {
    return result;
  }
  // 非对象返回自身
  if (!isObject(value)) {
    return value;
  }
  // isArr 判断是否是数组
  var isArr = isArray(value);
  if (isArr) {
    /// isArr 结果为 true,拷贝数组
    result = initCloneArray(value);
    if (!isDeep) {
      return copyArray(value, result);
    }
  } else {
    // isArr 结果为 false
    // 获取 Tag,实质上是 Object.prototype.toString.call(value),获取的变量类型
    var tag = getTag(value),
        isFunc = tag == funcTag || tag == genTag;
    // 拷贝 Array Buffer
    if (isBuffer(value)) {
      return cloneBuffer(value, isDeep);
    }
    // 类型是 object,开始拷贝 object
    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 {
      // 判断是否是 Error、Function、WeakMap,注意4.17.20 版本 HTMLElement 没有加入 cloneableTags 中,返回 undefined 
      if (!cloneableTags[tag]) {
        return object ? value : {};
      }
      // 根据 Tag 利用 initCloneByTag 拷贝新值并传入 result(创建 result)
      result = initCloneByTag(value, tag, isDeep);
    }
  }
  // Check for circular references and return its corresponding clone.
  // 检查是否循环引用(检查 stack 内是否有对应值),如果不是,暂时存储进 stack 的值;如果是,返回 stack 的值
  stack || (stack = new Stack);
  var stacked = stack.get(value);
  if (stacked) {
    return stacked;
  }
  stack.set(value, result);
  // set 和 map 拷贝,同样是获取 flag,利用 set 和 map 的内置方法     
  if (isSet(value)) {
    value.forEach(function(subValue) {
      result.add(baseClone(subValue, bitmask, customizer, subValue, value, stack));
    });
  } else if (isMap(value)) {
    value.forEach(function(subValue, key) {
      result.set(key, baseClone(subValue, bitmask, customizer, key, value, stack));
    });
  }
  // 深拷贝这里 isFull isFlat 都是 false,因此 keysFunc === keys,函数用于判断是否类数组
  var keysFunc = isFull
  ? (isFlat ? getAllKeysIn : getAllKeys)
  : (isFlat ? keysIn : keys);
  var props = isArr ? undefined : keysFunc(value);
  // 类数组用 for 循环将 prop 一一加入到 result 上
  arrayEach(props || value, function(subValue, key) {
    if (props) {
      key = subValue;
      subValue = value[key];
    }
    // 将键值递归的传入 result(baseClone的结果)
    assignValue(result, key, baseClone(subValue, bitmask, customizer, key, value, stack));
  });
  return result;
}

通过查看源码,可以看到判断类型的函数有 isObjectisArrayisSetisMapgetTag 等, 拷贝数据的函数有 initCloneObjectinitCloneArrayinitCloneByTag 等 下面拿其中的 isObjectinitCloneObject 分析

getTag

这里的 objectToString 就是 Object.prototype.toString.call()

/**
     * The base implementation of `getTag` without fallbacks for buggy environments.
     *
     * @private
     * @param {*} value The value to query.
     * @returns {string} Returns the `toStringTag`.
     */
function baseGetTag(value) {
  if (value == null) {
    return value === undefined ? undefinedTag : nullTag;
  }
  return (symToStringTag && symToStringTag in Object(value))
    ? getRawTag(value)
  : objectToString(value);
}
// 这里的 objectToString 就是 Object.prototype.toString.call()

initCloneArray

代码里 new array.constructor 实现了利用参数 value 的构造函数新建对象,后面的 indexinput 是对 RegExp.prototype.exec 操作后的数组的一个补充

/**
     * Initializes an array clone.
     *
     * @private
     * @param {Array} array The array to clone.
     * @returns {Array} Returns the initialized clone.
     */
function initCloneArray(array) {
  var length = array.length,
      result = new array.constructor(length);
  // Add properties assigned by `RegExp#exec`.
  if (length && typeof array[0] == 'string' && hasOwnProperty.call(array, 'index')) {
    result.index = array.index;
    result.input = array.input;
  }
  return result;
}

总结

lodash 的内部函数 baseClone 实现了具体的 cloneDeep 函数,同时为了优化整体的包大小;采用了传入参数的方式,让 baseClone 可以实现 clonecloneWith 等功能,代码的复用度极高,这些函数的具体实现思路会在之后分析

  1. 为了判断类型,lodash 实现了由 is__ 开头的一系列函数,利用内置方法比如 Array.isArray
  2. getTag 这个方法用 Object.prototype.toString.call 对变量判断类型,该方法的缺点是只能判断内置类型,不能判断用户的自定义类型(自定义类型一律会返回 [object object]
  3. 为了拷贝内置类型和自定义类型的数据,lodash实现了 initClone 系列函数,内部都使用了传入参数的 constructor 来构建新的数据