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)
最后,所有文章都会同步发送到微信公众号上,欢迎关注,欢迎提意见:
作者:对角另一面