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 是实现 clonecloneDeep 等一系列复制函数的内部函数,也是这些复制函数的核心。

前期准备

逻辑运算符

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_FLAGCLONE_FLAT_FLAGCLONE_SYMBOLS_FLAG 几个变量,并且在 baseClone 函数的开始就和 bitmask 来做逻辑与,也就是二进制运算,得到 isDeepisFlatisFull 几个标志位。

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

因此如果要同时开启 isDeepisFlatbitmask 只需要传入 CLONE_DEEP_FLAG | CLONE_FLAT_FLAG ,因为这保证了第一位和第二位都为 1isDeepisFlat 必定为真值。

因此,这三个状态位的开启关闭组合,完全可以通过三个 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 后的值,用来识别对象的类型。

可以看到,ErrorWeakMap 类型都是不可复制的。其实除了这两个外,WeakSetDOM 对象都是不可以复制的,因为这些类型没有标记为 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 属性。

如果是这种类型的数组,在初始化的时候,将 indexinput 属性进行复制。

参数说明

value

要复制的 value

bitmask

控制 isDeepisFlatisFull 标记的二进制值。

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 为假值,则使用 copyArrayvalue 中每一项都复制到 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)

前面两个条件分别是判断是否为 ObjectArgumentsisFunc && !Object 的意思是只传入了一个 function ,即类似于这样的调用方式:

baseClone(function () {})

isFlat 为真值,或者 isFunctrue也即传入的 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 中即可。

从以上的分析中可以看到,如果单纯传入的 valuefunction 类型,即类似 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)
  }
}

在上一节如果 valueFunction 并且没有传入 object 时,得到的结果是对象。

在这个分支里,如果 valueFunction 并且有传 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)

解决的思路其实很简单,以原始值 valuekey,复制的结果 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
}

调用 valueforEach 方法遍历,因为上面调用 initCloneByTag 的时候已经创建了一个 Map 的实例容器 result ,在遍历的过程中,调用 baseClonesubValue 复制,也即对 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 函数。

这里就是根据 isFullisFlat 不同的情况决定使用那一个函数来收集属性。

如果是数组,则不收集属性,如果不是,则收集属性到 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 ,这样就和数组遍历时的 subValuekey 含义对应起来。

接着递归调用 baseClonesubValue 进行复制,然后使用 assignValue 将复制后的值设置到结果 result 对应的 key 上,这样就达到了数组和对象复制的目的。

License

署名-非商业性使用-禁止演绎 4.0 国际 (CC BY-NC-ND 4.0)

最后,所有文章都会同步发送到微信公众号上,欢迎关注,欢迎提意见:

作者:对角另一面

results matching ""

    No results matching ""