lodash 源码解析 -- clone

446 阅读7分钟

lodash 版本 4.17.20

前言

前一篇文章中简单的分析了 cloneDeep 和其底层实现 baseClone 函数,这次继续分析 clone 函数,同时继续展开说说 baseClone 这个所有 clone 系列函数的底层实现

lodash 库中的 clone 函数是用于将变量复制的函数,这个函数经常应用于对未知变量的浅层复制,比如对于像是 Redux 这样的库中的不可变数据特性,要保证迭代数据就要传入新的值,同时又不想将底层数据全部更新,此时进行一次浅克隆,可以只改变迭代需要的 state 的指针改变,内部的属性都没有改变,就是比较理想的处理

函数作用

对参数传入的值进行浅克隆。方法基于结构化克隆标准,只克隆参数对象的可枚举属性,支持 ArrayArrayBufferStringNumberBooleanObjectSetMapTypedArraySymbolRegexesDateObject 等类型。不可复制类型包含 ErrorDOMWeakMapFunction 等,最终将返回空对象。 思路分析

  1. 流程图 原图

  2. 主要应当解决的问题:循环引用,即对象自己引用自己的问题

源码分析

clone

底层基于 baseClone 实现,通过传入 CLONE_SYMBOLS_FLAG 只启用浅克隆的功能

function clone(value) {
  return baseClone(value, CLONE_SYMBOLS_FLAG);
}

1. 传入参数

传入需要被克隆的变量值

2. 代码分析

baseClone(value, CLONE_SYMBOLS_FLAG)

这里的 baseClone 传入两个参数:valueCLONE_SYMBOLS_FLAG ,其中 CLONE_SYMBOLS_FLAG 是表示启用 baseClone 的复制功能的标志位

baseClone

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;
  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;
    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]) {
        return object ? value : {};
      }
      result = initCloneByTag(value, tag, isDeep);
    }
  }
  // Check for circular references and return its corresponding clone.
  stack || (stack = new Stack);
  var stacked = stack.get(value);
  if (stacked) {
    return stacked;
  }
  stack.set(value, result);
  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));
    });
  }
  var keysFunc = isFull
  ? (isFlat ? getAllKeysIn : getAllKeys)
  : (isFlat ? keysIn : keys);
  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));
  });
  return result;
}

1. 传入参数分析

  • baseClone 共传入 6 个参数 valuebitmaskcustomizerkeyobjectstack
  • value 是需要被克隆的值
  • bitmask 是标志位,共三个标志 CLONE_DEEP_FLAGCLONE_FLAT_FLAGCLONE_SYMBOLS_FLAG,分别是 001、010、100 三个二进制数,转化为十进制数就是 1、2、4,在 baseClone 代码当中 bitmask 分别再与这三个标志位进行逻辑与操作得到标志位。这个操作可以很方便的得出标志位,节省函数传参,据此运行对应代码,比如 cloneDeep 源码中,CLONE_DEEP_FLAGCLONE_SYMBOLS_FLAG 进行逻辑或操作,传入后再各自进行逻辑与操作,就得到了相应标志位。
  • customizercloneWithcloneDeepWith 中的自定义克隆函数,因为 clonecloneDeep 还有无法克隆的数据,因此如果要克隆这些数据就需要用到自定义的克隆函数,比如 DOM 节点,这时就可以传入 document.cloneNode 类似的自定义克隆函数
  • key 是当需要深克隆,克隆上一层时传入的 key
  • object 是当被克隆数值是不可复制值时,如果处在递归当中就返回 ,如果没有就返回空对象
  • stack 用于解决循环引用问题,当对象的一个属性中引用了自己,就构成了循环引用,这里的 stack 可以通过对引用收集,并递归传递的方式将自我引用这个过程重现并在递归之后赋予引用,避免一直递归下去的问题

2. 代码分析

首先是对标志位的判断,用 & 运算符可以在位运算的层面上将获取到对应的标志位,以及 cloneWithcloneDeepWith 的判断,如果这里存在 customizer 就执行 customizer 并返回值,这里不详细分析

// ...  
var result,
      isDeep = bitmask & CLONE_DEEP_FLAG,
      isFlat = bitmask & CLONE_FLAT_FLAG,
      isFull = bitmask & CLONE_SYMBOLS_FLAG;
  if (customizer) {
    result = object ? customizer(value, key, object, stack) : customizer(value);
  }
  if (result !== undefined) {
    return result;
  }
// ...

接下来的对是否为非引用值做判断,这里的 isObject 如果是 nullundefinedstringnumberboolean 这类非引用值会返回 false,注意如果是包装对象仍然会返回 true

// ...  
if (!isObject(value)) {
    return value;
}
// ...

这里的 isObject 使用的是原生的 typeof ,代码如下,因为原生语言的问题,JS 的 null 会显示为 'object',因此这里会加判断 value != null

function isObject(value) {
  var type = typeof value;
  return value != null && (type == 'object' || type == 'function');
}

判数组用的是 isArray 函数,这个函数是引用了原生的 Array.isArray 函数

var isArray = Array.isArray; // 第 11286 行

复制数组,这里用了 initCloneArray,当复制数组成功后,判断是否需要深复制,如果不需要,就直接将被复制对象的属性值一一复制进新数组即可

// ...
var isArr = isArray(value);
if (isArr) {
  result = initCloneArray(value);
  if (!isDeep) {
    return copyArray(value, result);
  }
}
// ...

获取克隆对象的类型,使用 getTag 的方式,使用了 Object.prototype.toString.call 的结果来判断类型,这个方法会使用对象上的 Symbol.toStringTag 属性(如果对象上有的话),lodash 也会判断是否有此属性,并据此属性做出判断,getTag 函数里调用的 getRawTag 就是判断 Symbol.toStringTag 的方法

//..
var tag = getTag(value),
    isFunc = tag == funcTag || tag == genTag;
//..
// getTag
// function baseGetTag(value) {
//   if (value == null) {
//     return value === undefined ? undefinedTag : nullTag;
//   }
//   return (symToStringTag && symToStringTag in Object(value))
//    ? getRawTag(value)
//    : objectToString(value);
// }

根据 isBuffer 函数判断是否为 ArrayBuffer 类型,并返回 cloneBuffer 的值

//...
if (isBuffer(value)) {
  return cloneBuffer(value, isDeep);
}
//...

根据类型克隆对象,会同时判断是否是不可复制的类型,比如 WeakMapDOMErrorFunction

//...  
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]) {
    return object ? value : {};
  }
  result = initCloneByTag(value, tag, isDeep);
}
//...

判断循环引用,当 stack 不存在时创建 stack。当 stack存在时,查询 stack 内是否存在当前的被克隆值,如果存在说明是循环引用,直接返回 stack 内存储的值。stack 是 lodash 内部实现的缓存类型,内部是利用数组将 Map 方法安全的实现了一遍,方便查询和存储

//...  
stack || (stack = new Stack);
var stacked = stack.get(value);
if (stacked) {
  return stacked;
}
stack.set(value, result);
//...

判断是否是 Set 或 Map,如果是的话就调用各自的方法复制,同时递归调用 baseClone 预防循环引用等边界情况

//...
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));
  });
}
//...

判断是否是启用了 flat 功能,如果没有就使用 keysFunc 的函数将所有属性的 key 值复制出来,并通过 assignValue 的方式复制,注意 keysFunc 会用 · 判断属性是否是写在被克隆值本身而不是其原型上。assignValue 则采用 Object.defineProperty 的方式将属性值一一写入,如果没有 Object.defineProperty 则再使用普通的 k-v 赋值方法,增强了稳定性和健壮性

// ..
var keysFunc = isFull
? (isFlat ? getAllKeysIn : getAllKeys)
: (isFlat ? keysIn : keys);
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));
});
return result;
//...

总结

  1. clone 利用了 baseClone 中的浅层克隆的部分逻辑实现,利用维护 stack 的方式解决了循环引用问题,利用了 new constructor 的方式解决了克隆对象的问题
  2. 为了控制代码运行的同时减少参数传递,baseClone 使用了位运算的方式,也就是将各个标志位用二进制的方式传入,节省了参数
  3. baseClone 考虑了很多边界情况,比如原生方法的可靠性处理,比如使用库内实现的 stack 代替 Map 使用