Lodash源码阅读-baseClone

Lodash 源码阅读-baseClone

概述

baseClone 是 Lodash 库中实现深浅拷贝功能的核心函数,它作为 _.clone_.cloneDeep 等公开 API 的内部实现基础。这个函数支持自定义拷贝行为、处理循环引用、拷贝特殊对象类型等高级功能,是 Lodash 中最复杂也最强大的工具函数之一。

前置学习

依赖函数

baseClone 依赖的主要函数包括:

  • Stack:用于检测循环引用的堆栈数据结构
  • arrayEach:数组遍历函数
  • assignValue:给对象分配值的函数
  • baseAssign/baseAssignIn:对象属性分配函数
  • cloneBuffer:Buffer 类型克隆函数
  • copyArray:数组复制函数
  • copySymbols/copySymbolsIn:复制 Symbol 属性的函数
  • getAllKeys/getAllKeysIn:获取对象所有键的函数
  • getTag:获取对象类型标签的函数
  • initCloneArray:初始化数组克隆的函数
  • initCloneByTag:根据对象类型标签初始化克隆的函数
  • initCloneObject:初始化对象克隆的函数
  • isArrayisBufferisMapisObjectisSet 等类型检查函数

技术知识

  • 位掩码(Bitmasks):使用二进制位操作来设置和检查多个布尔标志
  • 深拷贝与浅拷贝:对象拷贝的不同级别及其实现方式
  • 循环引用处理:处理对象中的循环引用问题
  • 特殊对象类型处理:处理 JavaScript 中各种特殊对象类型的拷贝
  • 递归算法:用于深度遍历对象结构

源码实现

javascript 复制代码
/**
 * 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
 *  2 - Flatten inherited properties
 *  4 - Clone symbols
 * @param {Function} [customizer] The function to customize cloning.
 * @param {string} [key] The key of `value`.
 * @param {Object} [object] The parent object of `value`.
 * @param {Object} [stack] Tracks traversed objects and their clone counterparts.
 * @returns {*} Returns the cloned value.
 */
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;
}

实现思路

baseClone 函数的实现思路非常系统化,可以概括为以下几个步骤:

  1. 解析位掩码标志:解释传入的 bitmask 参数,确定是浅拷贝还是深拷贝、是否拷贝原型链上的属性、是否拷贝 Symbol 属性等
  2. 应用自定义克隆函数:如果提供了 customizer 函数,优先使用它处理值
  3. 处理基本类型:如果值是基本类型(非对象),直接返回它
  4. 处理数组:对数组类型进行特殊处理
  5. 处理对象:根据对象的具体类型(普通对象、Buffer、特殊对象如 Date、RegExp 等)选择不同的克隆策略
  6. 处理循环引用:使用 Stack 数据结构检测和处理循环引用
  7. 处理特殊集合:对 Set 和 Map 类型的特殊处理
  8. 深度克隆子属性:对对象的属性或数组的元素递归应用 baseClone
  9. 返回克隆结果:返回完整克隆的值

这种设计使得 baseClone 能够处理 JavaScript 中几乎所有类型的值的克隆,同时保持灵活性和可定制性。

源码解析

函数签名和参数解析

javascript 复制代码
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;

函数接收六个参数:

  • value:要克隆的值
  • bitmask:控制克隆行为的位掩码标志
  • customizer:可选的自定义克隆函数
  • key:当前值在其父对象中的键名
  • object:父对象
  • stack:用于跟踪已克隆对象的堆栈

位掩码的解析使用了位运算:

  • isDeep = bitmask & CLONE_DEEP_FLAG:检查是否为深克隆(值为 1)
  • isFlat = bitmask & CLONE_FLAT_FLAG:检查是否包含原型链上的属性(值为 2)
  • isFull = bitmask & CLONE_SYMBOLS_FLAG:检查是否包含 Symbol 属性(值为 4)

例如,要进行包含 Symbol 的深克隆,bitmask 值为 5(CLONE_DEEP_FLAG | CLONE_SYMBOLS_FLAG)。

自定义克隆器处理

javascript 复制代码
if (customizer) {
  result = object ? customizer(value, key, object, stack) : customizer(value);
}
if (result !== undefined) {
  return result;
}

这部分检查是否提供了自定义克隆函数 customizer

  • 如果提供了,则调用该函数处理当前值
  • 根据上下文不同,要么传入完整的参数集 (value, key, object, stack),要么只传入 value
  • 如果自定义函数返回了非 undefined 的结果,直接返回该结果,中断默认克隆流程

这提供了极大的灵活性,允许开发者自定义任何特殊类型的克隆行为。

基本类型和数组处理

javascript 复制代码
if (!isObject(value)) {
  return value;
}
var isArr = isArray(value);
if (isArr) {
  result = initCloneArray(value);
  if (!isDeep) {
    return copyArray(value, result);
  }
}

这部分先检查是否为非对象类型(如字符串、数字等):

  • 如果是基本类型,直接返回原值,因为基本类型是按值传递的
  • 如果是数组,使用 initCloneArray 初始化一个新数组
  • 对于浅克隆,直接调用 copyArray 复制数组元素并返回
  • 对于深克隆,会在后续代码中处理数组的每个元素

特殊对象类型处理

javascript 复制代码
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);
  }
}

这段代码处理非数组的对象类型:

  1. 获取对象的类型标签(如 [object Object][object Date] 等)
  2. 对 Buffer 类型进行特殊处理
  3. 处理普通对象、Arguments 对象和函数:
    • 根据 isFlatisFunc 标志决定是创建普通空对象还是通过 initCloneObject 创建(后者会保留原型链)
    • 对于浅克隆,复制属性后直接返回
  4. 处理其他特殊对象类型(如 Date, RegExp, Map, Set 等):
    • 检查对象的类型标签是否在 cloneableTags
    • 使用 initCloneByTag 根据对象类型选择适当的克隆方法

循环引用处理

javascript 复制代码
// 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);

这部分代码处理循环引用问题:

  1. 如果没有提供 stack 参数,创建一个新的 Stack 实例
  2. 检查当前值 value 是否已经在克隆过程中出现过
  3. 如果找到对应的已克隆对象,直接返回该对象,避免无限递归
  4. 否则,将当前值与其克隆结果保存到 stack 中

这种机制确保即使对象中存在循环引用(如 obj.prop = obj),克隆过程也不会陷入无限递归。

集合类型处理

javascript 复制代码
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)
    );
  });
}

这段代码专门处理 Set 和 Map 这两种特殊的集合类型:

  • 对于 Set 类型,遍历其所有值,克隆每个值并添加到结果 Set 中
  • 对于 Map 类型,遍历其所有键值对,克隆值并设置到结果 Map 中,键保持不变
  • 在处理它们的值时,递归调用 baseClone 以确保深度克隆

属性克隆

javascript 复制代码
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. 根据 isFullisFlat 标志选择适当的函数来获取对象的键:
    • isFull(克隆 Symbol)和 isFlat(包含原型链)的组合决定使用的函数
    • 可能的函数有:getAllKeysIngetAllKeyskeysInkeys
  2. 对于数组,直接遍历数组元素;对于对象,遍历获取的属性列表
  3. 对每个子值递归调用 baseClone,并将结果赋值给克隆对象的对应属性
  4. 最后返回完全克隆的对象

这种设计确保了所有嵌套的对象和数组都能被正确地递归克隆。

总结

baseClone 函数是 Lodash 中实现对象克隆的核心函数,它通过一系列精心设计的代码处理了 JavaScript 中几乎所有类型的值的克隆问题。这个函数展示了几个重要的编程设计原则:

  1. 抽象层次划分:将复杂逻辑分解为处理不同类型值的专门函数
  2. 位掩码优化:使用位运算来有效地表示和处理多个布尔标志
  3. 循环引用处理:使用 Stack 数据结构优雅地解决循环引用问题
  4. 类型特殊处理:针对不同的对象类型提供专门的处理逻辑
  5. 可扩展性:通过 customizer 参数支持自定义克隆行为

baseClone 函数的实现复杂但组织有序,展示了如何在 JavaScript 中构建一个既健壮又灵活的对象克隆系统。它处理了许多边缘情况,并为开发者提供了丰富的定制选项,是学习 JavaScript 高级编程的绝佳案例。

相关推荐
Jinxiansen02119 分钟前
Vue 3 实战:【加强版】公司通知推送(WebSocket + token 校验 + 心跳机制)
前端·javascript·vue.js·websocket·typescript
MrSkye10 分钟前
React入门:组件化思想?数据驱动?
前端·react.js·面试
BillKu19 分钟前
Java解析前端传来的Unix时间戳
java·前端·unix
@Mr_LiuYang19 分钟前
网页版便签应用开发:HTML5本地存储与拖拽交互实践
前端·交互·html5·html5便签应用
JohnYan22 分钟前
Bun技术评估 - 05 SQL
javascript·后端·bun
JacksonGao23 分钟前
一分钟带你了解React Fiber的工作单元结构!
前端·react.js
前端农民晨曦24 分钟前
深入浏览器事件循环与任务队列架构
前端·javascript·面试
Vhen26 分钟前
Taro Echarts封装内外环形饼图
前端
Spider_Man40 分钟前
JavaScript对象那些坑:初学者必踩的“陷阱”与进阶秘籍
前端·javascript
海螺先生1 小时前
Cursor 高阶使用指南:AI 辅助开发的深度整合
前端