lodash 代码版本 4.7.20
函数作用
cloneDeep 作用是将变量数据中所有的值,都依次拷贝一份新的出来,包括但不限于 arrays,array buffers ,booleans, Date, maps, numbers,Object,regexes,sets,strings,symbols,typed arrays。注意只会拷贝对象的可枚举属性。如果对象不可拷贝,比如是 Error、Function、DOM、WeakMap,那么返回空对象。和 clone 函数有所不同,clone 只拷贝一层,cloneDeep 会递归拷贝到对象下一层直到不能递归为止
思路分析
cloneDeep 需要关注的几个问题:类型判断、循环引用,内置类型可以用调用 Object.prototype.toString.call 这个方法来判断类型,自定义类型的拷贝可以使用 new constructor 的方式拷贝
源码分析
cloneDeep
首先打开 cloneDeep 函数的文件发现,其底层是由 baseClone 实现的, 实际上 clone、cloneDeep、clon 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;
}
通过查看源码,可以看到判断类型的函数有 isObject,isArray,isSet,isMap,getTag 等,
拷贝数据的函数有 initCloneObject、initCloneArray 、initCloneByTag 等
下面拿其中的 isObject 和 initCloneObject 分析
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 的构造函数新建对象,后面的 index 和 input 是对 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 可以实现 clone、cloneWith 等功能,代码的复用度极高,这些函数的具体实现思路会在之后分析
- 为了判断类型,lodash 实现了由
is__开头的一系列函数,利用内置方法比如Array.isArray getTag这个方法用Object.prototype.toString.call对变量判断类型,该方法的缺点是只能判断内置类型,不能判断用户的自定义类型(自定义类型一律会返回[object object])- 为了拷贝内置类型和自定义类型的数据,lodash实现了
initClone系列函数,内部都使用了传入参数的constructor来构建新的数据