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
:初始化对象克隆的函数isArray
、isBuffer
、isMap
、isObject
、isSet
等类型检查函数
技术知识
- 位掩码(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
函数的实现思路非常系统化,可以概括为以下几个步骤:
- 解析位掩码标志:解释传入的 bitmask 参数,确定是浅拷贝还是深拷贝、是否拷贝原型链上的属性、是否拷贝 Symbol 属性等
- 应用自定义克隆函数:如果提供了 customizer 函数,优先使用它处理值
- 处理基本类型:如果值是基本类型(非对象),直接返回它
- 处理数组:对数组类型进行特殊处理
- 处理对象:根据对象的具体类型(普通对象、Buffer、特殊对象如 Date、RegExp 等)选择不同的克隆策略
- 处理循环引用:使用 Stack 数据结构检测和处理循环引用
- 处理特殊集合:对 Set 和 Map 类型的特殊处理
- 深度克隆子属性:对对象的属性或数组的元素递归应用 baseClone
- 返回克隆结果:返回完整克隆的值
这种设计使得 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);
}
}
这段代码处理非数组的对象类型:
- 获取对象的类型标签(如
[object Object]
,[object Date]
等) - 对 Buffer 类型进行特殊处理
- 处理普通对象、Arguments 对象和函数:
- 根据
isFlat
和isFunc
标志决定是创建普通空对象还是通过initCloneObject
创建(后者会保留原型链) - 对于浅克隆,复制属性后直接返回
- 根据
- 处理其他特殊对象类型(如 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);
这部分代码处理循环引用问题:
- 如果没有提供
stack
参数,创建一个新的 Stack 实例 - 检查当前值
value
是否已经在克隆过程中出现过 - 如果找到对应的已克隆对象,直接返回该对象,避免无限递归
- 否则,将当前值与其克隆结果保存到 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;
这部分代码处理对象的属性或数组的元素:
- 根据
isFull
和isFlat
标志选择适当的函数来获取对象的键:isFull
(克隆 Symbol)和isFlat
(包含原型链)的组合决定使用的函数- 可能的函数有:
getAllKeysIn
、getAllKeys
、keysIn
和keys
- 对于数组,直接遍历数组元素;对于对象,遍历获取的属性列表
- 对每个子值递归调用
baseClone
,并将结果赋值给克隆对象的对应属性 - 最后返回完全克隆的对象
这种设计确保了所有嵌套的对象和数组都能被正确地递归克隆。
总结
baseClone
函数是 Lodash 中实现对象克隆的核心函数,它通过一系列精心设计的代码处理了 JavaScript 中几乎所有类型的值的克隆问题。这个函数展示了几个重要的编程设计原则:
- 抽象层次划分:将复杂逻辑分解为处理不同类型值的专门函数
- 位掩码优化:使用位运算来有效地表示和处理多个布尔标志
- 循环引用处理:使用 Stack 数据结构优雅地解决循环引用问题
- 类型特殊处理:针对不同的对象类型提供专门的处理逻辑
- 可扩展性:通过 customizer 参数支持自定义克隆行为
baseClone
函数的实现复杂但组织有序,展示了如何在 JavaScript 中构建一个既健壮又灵活的对象克隆系统。它处理了许多边缘情况,并为开发者提供了丰富的定制选项,是学习 JavaScript 高级编程的绝佳案例。