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 高级编程的绝佳案例。

相关推荐
范文杰3 小时前
AI 时代如何更高效开发前端组件?21st.dev 给了一种答案
前端·ai编程
拉不动的猪3 小时前
刷刷题50(常见的js数据通信与渲染问题)
前端·javascript·面试
拉不动的猪3 小时前
JS多线程Webworks中的几种实战场景演示
前端·javascript·面试
FreeCultureBoy4 小时前
macOS 命令行 原生挂载 webdav 方法
前端
uhakadotcom4 小时前
Astro 框架:快速构建内容驱动型网站的利器
前端·javascript·面试
uhakadotcom4 小时前
了解Nest.js和Next.js:如何选择合适的框架
前端·javascript·面试
uhakadotcom4 小时前
React与Next.js:基础知识及应用场景
前端·面试·github
uhakadotcom4 小时前
Remix 框架:性能与易用性的完美结合
前端·javascript·面试
uhakadotcom5 小时前
Node.js 包管理器:npm vs pnpm
前端·javascript·面试
LaoZhangAI6 小时前
2025最全GPT-4o图像生成API指南:官方接口配置+15个实用提示词【保姆级教程】
前端