lodash 版本 4.17.20
前言
前一篇文章中简单的分析了 cloneDeep 和其底层实现 baseClone 函数,这次继续分析 clone 函数,同时继续展开说说 baseClone 这个所有 clone 系列函数的底层实现
lodash 库中的 clone 函数是用于将变量复制的函数,这个函数经常应用于对未知变量的浅层复制,比如对于像是 Redux 这样的库中的不可变数据特性,要保证迭代数据就要传入新的值,同时又不想将底层数据全部更新,此时进行一次浅克隆,可以只改变迭代需要的 state 的指针改变,内部的属性都没有改变,就是比较理想的处理
函数作用
对参数传入的值进行浅克隆。方法基于结构化克隆标准,只克隆参数对象的可枚举属性,支持 Array、ArrayBuffer、String、Number、Boolean、Object、Set、Map、TypedArray、Symbol、Regexes、DateObject 等类型。不可复制类型包含 Error、DOM、WeakMap、Function 等,最终将返回空对象。
思路分析
-
流程图
原图
-
主要应当解决的问题:循环引用,即对象自己引用自己的问题
源码分析
clone
底层基于 baseClone 实现,通过传入 CLONE_SYMBOLS_FLAG 只启用浅克隆的功能
function clone(value) {
return baseClone(value, CLONE_SYMBOLS_FLAG);
}
1. 传入参数
传入需要被克隆的变量值
2. 代码分析
baseClone(value, CLONE_SYMBOLS_FLAG)
这里的 baseClone 传入两个参数:value 和 CLONE_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 个参数value、bitmask、customizer、key、object、stackvalue是需要被克隆的值bitmask是标志位,共三个标志CLONE_DEEP_FLAG、CLONE_FLAT_FLAG、CLONE_SYMBOLS_FLAG,分别是 001、010、100 三个二进制数,转化为十进制数就是 1、2、4,在baseClone代码当中bitmask分别再与这三个标志位进行逻辑与操作得到标志位。这个操作可以很方便的得出标志位,节省函数传参,据此运行对应代码,比如cloneDeep源码中,CLONE_DEEP_FLAG和CLONE_SYMBOLS_FLAG进行逻辑或操作,传入后再各自进行逻辑与操作,就得到了相应标志位。customizer是cloneWith和cloneDeepWith中的自定义克隆函数,因为clone和cloneDeep还有无法克隆的数据,因此如果要克隆这些数据就需要用到自定义的克隆函数,比如DOM节点,这时就可以传入document.cloneNode类似的自定义克隆函数key是当需要深克隆,克隆上一层时传入的key值object是当被克隆数值是不可复制值时,如果处在递归当中就返回 ,如果没有就返回空对象stack用于解决循环引用问题,当对象的一个属性中引用了自己,就构成了循环引用,这里的stack可以通过对引用收集,并递归传递的方式将自我引用这个过程重现并在递归之后赋予引用,避免一直递归下去的问题
2. 代码分析
首先是对标志位的判断,用 & 运算符可以在位运算的层面上将获取到对应的标志位,以及 cloneWith 和 cloneDeepWith 的判断,如果这里存在 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 如果是 null、undefined、string、number、boolean 这类非引用值会返回 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);
}
//...
根据类型克隆对象,会同时判断是否是不可复制的类型,比如 WeakMap、DOM、Error、Function
//...
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;
//...
总结
clone利用了baseClone中的浅层克隆的部分逻辑实现,利用维护stack的方式解决了循环引用问题,利用了new constructor的方式解决了克隆对象的问题- 为了控制代码运行的同时减少参数传递,baseClone 使用了位运算的方式,也就是将各个标志位用二进制的方式传入,节省了参数
baseClone考虑了很多边界情况,比如原生方法的可靠性处理,比如使用库内实现的stack代替Map使用