lodash源码分析之baseClone
本文为读 lodash 源码的第一百九十八篇,后续文章会更新到这个仓库中,欢迎 star:pocket-lodash
gitbook也会同步仓库的更新,gitbook地址:pocket-lodash
依赖
import Stack from './Stack.js'
import arrayEach from './arrayEach.js'
import assignValue from './assignValue.js'
import cloneBuffer from './cloneBuffer.js'
import copyArray from './copyArray.js'
import copyObject from './copyObject.js'
import cloneArrayBuffer from './cloneArrayBuffer.js'
import cloneDataView from './cloneDataView.js'
import cloneRegExp from './cloneRegExp.js'
import cloneSymbol from './cloneSymbol.js'
import cloneTypedArray from './cloneTypedArray.js'
import copySymbols from './copySymbols.js'
import copySymbolsIn from './copySymbolsIn.js'
import getAllKeys from './getAllKeys.js'
import getAllKeysIn from './getAllKeysIn.js'
import getTag from './getTag.js'
import initCloneObject from './initCloneObject.js'
import isBuffer from '../isBuffer.js'
import isObject from '../isObject.js'
import isTypedArray from '../isTypedArray.js'
import keys from '../keys.js'
import keysIn from '../keysIn.js'
《lodash源码分析之Stack》 《lodash源码分析之arrayEach》 《lodash源码分析之assignValue》 《lodash源码分析之cloneBuffer》 《lodash源码分析之copyArray》 《lodash源码分析之copyObject》 《lodash源码分析之cloneArrayBuffer》 《lodash源码分析之cloneDataView》 《lodash源码分析之cloneRegExp》 《lodash源码分析之cloneSymbol》 《lodash源码分析之cloneTypedArray》 《lodash源码分析之copySymbols》 《lodash源码分析之copySymbolsIn》 《lodash源码分析之getAllKeys》 《lodash源码分析之getAllKeysIn》 《lodash源码分析之getTag》 《lodash源码分析之initCloneObject》 《lodash源码分析之isBuffer》 《lodash源码分析之isObject》 《lodash源码分析之isTypedArray》 《lodash源码分析之keys》 《lodash源码分析之keysIn》
源码分析
baseClone 是实现 clone 、 cloneDeep 等一系列复制函数的内部函数,也是这些复制函数的核心。
前期准备
逻辑运算符
const CLONE_DEEP_FLAG = 1
const CLONE_FLAT_FLAG = 2
const CLONE_SYMBOLS_FLAG = 4
function baseClone (value, bitmask, customizer, key, object, stack) {
const isDeep = bitmask & CLONE_DEEP_FLAG
const isFlat = bitmask & CLONE_FLAT_FLAG
const isFull = bitmask & CLONE_SYMBOLS_FLAG
...
}
源码的开始就定义了 CLONE_DEEP_FLAG、CLONE_FLAT_FLAG 和 CLONE_SYMBOLS_FLAG 几个变量,并且在 baseClone 函数的开始就和 bitmask 来做逻辑与,也就是二进制运算,得到 isDeep 、isFlat 和 isFull 几个标志位。
isDeep 用来标记是否需要深拷贝,isFlat 用来标记是否需要拷贝原型链上的属性,isFull 用来标记是否需要拷贝 Symbol 类型的属性。
这里为什么要用到二进制运算呢?二进制运算在这里最大的好处是可以节约参数。
可以看到,这三个状态位是没有关联的,任何一个状态位的开关都和其他状态位无关,如果用布尔值来控制,则至需要三个参数。但是用二进制与运算只需要一个参数即可。
来看看是如何做到的。
先来看看这三个 FLAG 的二进制码是多少:
const CLONE_DEEP_FLAG = 1 // 001
const CLONE_FLAT_FLAG = 2 // 010
const CLONE_SYMBOLS_FLAG = 4 // 100
因此,如果我需要开启 isDeep ,只需要 bitmask 的第一位为 1 即可,最简单就是传入 001 ,即和 CLONE_DEEP_FLAG 的值相同。
同理,如果要开启 isFlat ,第二位为 1 即可,最简单也是传入 CLONE_FLAT_FLAG 即可。
我们又知道,二进制的与运算,只要相同位有一个为 1,则该位在与运算后改定为 1 。
因此如果要同时开启 isDeep 和 isFlat ,bitmask 只需要传入 CLONE_DEEP_FLAG | CLONE_FLAT_FLAG ,因为这保证了第一位和第二位都为 1 ,isDeep 和 isFlat 必定为真值。
因此,这三个状态位的开启关闭组合,完全可以通过三个 FLAG 的或组合来完成,通过一个参数 bitmask 传入,节约了两个参数。
可复制对象及不可复制对象
/** `Object#toString` result references. */
const argsTag = '[object Arguments]'
const arrayTag = '[object Array]'
const boolTag = '[object Boolean]'
const dateTag = '[object Date]'
const errorTag = '[object Error]'
const mapTag = '[object Map]'
const numberTag = '[object Number]'
const objectTag = '[object Object]'
const regexpTag = '[object RegExp]'
const setTag = '[object Set]'
const stringTag = '[object String]'
const symbolTag = '[object Symbol]'
const weakMapTag = '[object WeakMap]'
const arrayBufferTag = '[object ArrayBuffer]'
const dataViewTag = '[object DataView]'
const float32Tag = '[object Float32Array]'
const float64Tag = '[object Float64Array]'
const int8Tag = '[object Int8Array]'
const int16Tag = '[object Int16Array]'
const int32Tag = '[object Int32Array]'
const uint8Tag = '[object Uint8Array]'
const uint8ClampedTag = '[object Uint8ClampedArray]'
const uint16Tag = '[object Uint16Array]'
const uint32Tag = '[object Uint32Array]'
/** Used to identify `toStringTag` values supported by `clone`. */
const cloneableTags = {}
cloneableTags[argsTag] = cloneableTags[arrayTag] =
cloneableTags[arrayBufferTag] = cloneableTags[dataViewTag] =
cloneableTags[boolTag] = cloneableTags[dateTag] =
cloneableTags[float32Tag] = cloneableTags[float64Tag] =
cloneableTags[int8Tag] = cloneableTags[int16Tag] =
cloneableTags[int32Tag] = cloneableTags[mapTag] =
cloneableTags[numberTag] = cloneableTags[objectTag] =
cloneableTags[regexpTag] = cloneableTags[setTag] =
cloneableTags[stringTag] = cloneableTags[symbolTag] =
cloneableTags[uint8Tag] = cloneableTags[uint8ClampedTag] =
cloneableTags[uint16Tag] = cloneableTags[uint32Tag] = true
cloneableTags[errorTag] = cloneableTags[weakMapTag] = false
这里是初始化可复制对象及不可复制对象的标识字典。
后续会调用 getTag 获取对象 Object.prototype.toString 后的值,用来识别对象的类型。
可以看到,Error 和 WeakMap 类型都是不可复制的。其实除了这两个外,WeakSet ,DOM 对象都是不可以复制的,因为这些类型没有标记为 true 。
对于不可以复制的对象,会直接使用空对象来替换。
initCloneByTag
通过 Object.prototype.toString.call 获取到对象的 tag ,针对 tag 来判断不同的对象类型,来初始化复制的数据。
代码如下:
function initCloneByTag(object, tag, isDeep) {
const Ctor = object.constructor
switch (tag) {
case arrayBufferTag:
return cloneArrayBuffer(object)
case boolTag:
case dateTag:
return new Ctor(+object)
case dataViewTag:
return cloneDataView(object, isDeep)
case float32Tag: case float64Tag:
case int8Tag: case int16Tag: case int32Tag:
case uint8Tag: case uint8ClampedTag: case uint16Tag: case uint32Tag:
return cloneTypedArray(object, isDeep)
case mapTag:
return new Ctor
case numberTag:
case stringTag:
return new Ctor(object)
case regexpTag:
return cloneRegExp(object)
case setTag:
return new Ctor
case symbolTag:
return cloneSymbol(object)
}
}
这段代码的逻辑很简单,对于马上可以进行复制的数据,会马上进行复制,对于需要有特殊逻辑处理的数据,会先创建一个新的实例,后续再进行数据复制。
数组复制初始化
一般的数组在初始化时,直接使用 new Array(length) 初始化一个空数组即可。
但是如果使用正则的 exec 方法时,返回的结果也是一个数组,但是这个数组有两个比较特殊的属性,一个是 input ,保存原始的字符串,一个是 index ,保存上一次匹配文本的第一个字符的位置,因此也需要对这两个属性进行复制。
源码如下:
function initCloneArray(array) {
const { length } = array
const result = new array.constructor(length)
if (length && typeof array[0] === 'string' && hasOwnProperty.call(array, 'index')) {
result.index = array.index
result.input = array.input
}
return result
}
new array.constructor(length) 其实相当于 new Array(length) 。
接下来这段是判断传入的 array 是否为正则 exec 方法所返回的结果。
length && typeof array[0] === 'string' && hasOwnProperty.call(array, 'index')
如果是正则 exec 方法返回的结果,数组的第一个值肯定为匹配出来的字符串,并且自身有 index 属性。
如果是这种类型的数组,在初始化的时候,将 index 和 input 属性进行复制。
参数说明
value
要复制的 value 值
bitmask
控制 isDeep 、isFlat 和 isFull 标记的二进制值。
customizer
自定义值复制函数,如果传入了这个函数 ,则复制值的时候会直接使用这个函数复制,不使用 baseClone 里的复制逻辑。
key
传入 value 对应的属性 key 。
object
当前 value 所属的父级对象。
stack
Stack 类的实例,用来存储引用类的 value ,会用来避免循环依赖。
自定义复制函数
这是 baseClone 最先处理的复制逻辑,相关代码如下:
let result
if (customizer) {
result = object ? customizer(value, key, object, stack) : customizer(value)
}
if (result !== undefined) {
return result
}
有传 customizer 自定义复制函数参数时,baseClone 直接调用传入的 customizer 函数,得到结果 result ,因为复制逻辑已经由 customizer 处理了,baseClone 已经不需要额外的处理,直接将结果 result 返回即可。
这里有个需要注意的地方,如果没有传 value 父级 object 时,那 key 也没有意义,因此直接传入 value 即可,
简单数据类型复制
if (!isObject(value)) {
return value
}
对于简单的数据类型,因为不涉及到内存引用相同的问题,直接将同样的值返回即可。
数组浅复制
数组浅复制相关的源码如下:
const isArr = Array.isArray(value)
const tag = getTag(value)
if (isArr) {
result = initCloneArray(value)
if (!isDeep) {
return copyArray(value, result)
}
} else {
...
}
使用 Array.isArray 来判断 value 是否为数组,如果是数组,则用 initCloneArray 来初始化数组容器。
如果 isDeep 为假值,则使用 copyArray 将 value 中每一项都复制到 result 中,copyArray 会将 result 返回。
Buffer复制
Buffer 复制相关的源码如下:
if (isArr) {
...
} else {
const isFunc = typeof value === 'function'
if (isBuffer(value)) {
return cloneBuffer(value, isDeep)
}
}
使用 isBuffer 函数判断 value 是否为 Buffer 类型,如果是 Buffer 类型,则无论是否为浅复制还是深度复制,都调用 cloneBuffer 复制 value 。
Object、Arguments、Function 浅复制
if (isArr) {
...
} else {
...
if (tag == objectTag || tag == argsTag || (isFunc && !object)) {
result = (isFlat || isFunc) ? {} : initCloneObject(value)
if (!isDeep) {
return isFlat
? copySymbolsIn(value, copyObject(value, keysIn(value), result))
: copySymbols(value, Object.assign(result, value))
}
} else {
if (isFunc || !cloneableTags[tag]) {
return object ? value : {}
}
result = initCloneByTag(value, tag, isDeep)
}
}
先来看这个判断条件:
tag == objectTag || tag == argsTag || (isFunc && !object)
前面两个条件分别是判断是否为 Object 和 Arguments ,isFunc && !Object 的意思是只传入了一个 function ,即类似于这样的调用方式:
baseClone(function () {})
在 isFlat 为真值,或者 isFunc 为 true ,也即传入的 value 为函数,但是没有传入 object 的情况下,result 会被初始化成空对象,否则调用 initCloneObject 来初始化。
如果不需要深度复制,则在需要复制原型链,也即 isFlat 为真值的情况下:
copySymbolsIn(value, copyObject(value, keysIn(value), result))
可以看到,首先调用 keysIn(value) 将自身及原型链上所有非 Symbol 类型的可枚举属性收集,然后调用 copyObject 将非 Symbol 属性值复制到 result 中,再调用 copySymbolsIn 将自身及原型链上 Symbol 类型的可枚举属性值也复制到 result 中。
如果不需要复制原型链:
copySymbols(value, Object.assign(result, value))
就简单很多了,使用 Object.assign 复制自身可枚举的非 Symbol 属性值到 result 中,再调用 copySymbols 将自身可枚举的 Symbol 属性值复制到 result 中即可。
从以上的分析中可以看到,如果单纯传入的 value 为 function 类型,即类似 baseClone(function () {}) 时,得到的会是一个 object 类型,并不是函数。
不可复制数据类型的处理
相关源码如下:
if (isArr) {
...
} else {
...
if (tag == objectTag || tag == argsTag || (isFunc && !object)) {
...
} else {
if (isFunc || !cloneableTags[tag]) {
return object ? value : {}
}
result = initCloneByTag(value, tag, isDeep)
}
}
在上一节如果 value 为 Function 并且没有传入 object 时,得到的结果是对象。
在这个分支里,如果 value 为 Function 并且有传 object ,则得到的结果是 value 本身,即不会做任何复制。
我们在使用 clone 函数时,大部分情况下都不会直接传一个 function 去复制的,如果我们在复制一个包含函数的 object 时,得到的结果中,对应的函数还是指向原来的函数引用,即函数不会复制。
对于不可复制的数据类型的处理和函数的处理类似,如果直接传入一个不可复制的数据类型,会得到一个空对象,如果不可复制的数据类型包含是和 object 一起传入的,复制后,还是原来的值,即指向同一个引用。
如果不是以上的情况,则调用 initCloneByTag 函数初始化。
循环引用的解决
在深度复制的情况下,很容易出现循环引用的情况,baseClone 里使用 stack 来解决循环引用的问题。
相关代码如下:
stack || (stack = new Stack)
const stacked = stack.get(value)
if (stacked) {
return stacked
}
stack.set(value, result)
解决的思路其实很简单,以原始值 value 为 key,复制的结果 result 作为值,缓存到 stack 中。
在复制之前,先从 stack 中获取 value 的复制结果值,如果有值,表示之前已经对 value 做过复制,直接将结果返回即可。
如果没有值,则将 result 存入 stack 中。
Map的复制
相关代码如下:
if (tag == mapTag) {
value.forEach((subValue, key) => {
result.set(key, baseClone(subValue, bitmask, customizer, key, value, stack))
})
return result
}
调用 value 的 forEach 方法遍历,因为上面调用 initCloneByTag 的时候已经创建了一个 Map 的实例容器 result ,在遍历的过程中,调用 baseClone 将 subValue 复制,也即对 value 中每一个值都复制了一遍。
Set的复制
相关代码如下:
if (tag == setTag) {
value.forEach((subValue) => {
result.add(baseClone(subValue, bitmask, customizer, subValue, value, stack))
})
return result
}
Set 的复制和 Map 的复制类型,都是遍历,然后调用 baseClone 复制每个值,不再详述。
TypedArray 的复制
相关代码如下:
if (isTypedArray(value)) {
return result
}
因为在 initCloneByTag 中已经调用了 TypedArray 相关的复制方法复制,也即 result 就是复制后的值,直接返回即可。
数组及对象的深度复制
相关源码如下:
const keysFunc = isFull
? (isFlat ? getAllKeysIn : getAllKeys)
: (isFlat ? keysIn : keys)
const props = isArr ? undefined : keysFunc(value)
arrayEach(props || value, (subValue, key) => {
if (props) {
key = subValue
subValue = value[key]
}
assignValue(result, key, baseClone(subValue, bitmask, customizer, key, value, stack))
})
属性收集函数
在不同的情况下会不用不同的属性收集函数:
const keysFunc = isFull
? (isFlat ? getAllKeysIn : getAllKeys)
: (isFlat ? keysIn : keys)
const props = isArr ? undefined : keysFunc(value)
isFull 表示是否需要收集 Symbol 类型的属性。
如果需要,则使用 getAllKeysIn 或者 getAllKeys 函数。
如果不需要,则使用 keysIn 或者 keys 函数。
isFlat 表示是否需要收集原型链上的属性。
如果需要收集原型链上的属性,则使用 getAllKeysIn 或者 keysIn 函数。
如果不需要,则使用 getAllKeys 或者 keys 函数。
这里就是根据 isFull 和 isFlat 不同的情况决定使用那一个函数来收集属性。
如果是数组,则不收集属性,如果不是,则收集属性到 props 中。
遍历
arrayEach(props || value, (subValue, key) => {
if (props) {
key = subValue
subValue = value[key]
}
assignValue(result, key, baseClone(subValue, bitmask, customizer, key, value, stack))
})
使用 arrayEach 遍历属性集合 props 或者数组 value 。
如果 value 是数组,则每次遍历时, subValue 就是当前项的值,key 则为当前索引值。
如果 props 存在,则表示当前遍历的不是数组,而是对象,则 subValue 为当前的属性,使用 subValue 覆盖变量 key ,再从 value 中将 key 的值取出,覆盖变量 subValue ,这样就和数组遍历时的 subValue 和 key 含义对应起来。
接着递归调用 baseClone 对 subValue 进行复制,然后使用 assignValue 将复制后的值设置到结果 result 对应的 key 上,这样就达到了数组和对象复制的目的。
License
署名-非商业性使用-禁止演绎 4.0 国际 (CC BY-NC-ND 4.0)
最后,所有文章都会同步发送到微信公众号上,欢迎关注,欢迎提意见:
作者:对角另一面