lodash源码浅析之如何实现深拷贝

😄本文首发于: lodash-source-learning/

1、概要

工具库 lodash 在开发过程当中为咱们封装了丰富便捷的js函数,实现一些经常使用的功能,在使用过程当中就会对lodash的内部实现原理感到好奇。javascript

本次文章的主要内容分析阅读了lodash中深拷贝 _.cloneDeep()的实现。html

2、深拷贝和浅拷贝之间的区别

浅拷贝:对于引用类型的数据来讲,赋值运算只是更改了引用的指针,可是指针指向的地址仍是同一个,因此对应的变更会影响双方。java

深拷贝:递归拷贝一个对象中的字对象,完成后两个对象不互相影响。node

3、什么样的数据在深拷贝适用范围

包括但不限于:git

  • Date对象
  • Object
  • Array
  • TypedArray
  • Map
  • Set
  • ArrayBuffer
  • RegExp

4、lodash如何实现深拷贝

一、初始化

const CLONE_DEEP_FLAG = 1
const CLONE_SYMBOLS_FLAG = 4

function deepClone(value) {
  return baseClone(value, CLONE_DEEP_FLAG | CLONE_SYMBOLS_FLAG)
}

cloneDeep的主体函数baseClone:github

function baseClone(value, bitmask, customizer, key, object, stack) {
  let result
  const isDeep = bitmask & CLONE_DEEP_FLAG
  const isFlat = bitmask & CLONE_FLAT_FLAG
  const isFull = bitmask & CLONE_SYMBOLS_FLAG
}

以上入口代码看起来很简洁:定义两个位掩码常量,经过位运算控制参数类型,达到控制参数权限的基本实现:api

1 | 4 & 1 => 1  
1 | 4 & 2 => 0 
1 | 4 & 4 => 4

由上面的位元算可得知,在当前深拷贝模式下,isDeepisFulltrue,这两个变量在下面的代码中起到很大的判断做用。数组

关于javascript中位运算能够参考MDN:Bitwise_Operators缓存

二、标记值的类型

const tag = getTag(value)
const toString = Object.prototype.toString

function getTag(value) {
  if (value == null) {
    return value === undefined ? '[object Undefined]' : '[object Null]'
  }
  return toString.call(value)
}

以上实现经过调用Object的原型toString()方法,区别不一样value对应的具体类型:数据结构

var toString = Object.prototype.toString;
 toString.call(new Date); // [object Date]
 toString.call(new String); // [object String]
 toString.call(Math); // [object Math]
 //JavaScript版本1.8.5 及以上
 toString.call(undefined); // [object Undefined]
 toString.call(null); // [object Null]
 toString.call(argument); // [object Arguments]

三、数组的拷贝

if (isArr) {
    // 数组深拷贝的初始化,返回了一个新数组的雏形
    result = initCloneArray(value)
}
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
}

export default initCloneArray

看到这里会有疑问,为何数组类型的拷贝,须要判断typeof array[0] === 'string' && hasOwnProperty.call(array, 'index')indexinput是什么状况?

熟悉js正则匹配的会知道,这里考虑了一种特殊的数组状况,那就是regexObj.exec(str),用来处理匹配正则时,执行exec()的返回结果状况,若是匹配成功,exec() 方法返回一个数组(包含额外的属性 indexinput

const matches = /(hello \S+)/.exec('hello world, javascript');
console.log(matches);
输出=>
[
    0: "hello world,"
    1: "hello world,"
    index: 0
    input: "hello world, javascript"
    groups: undefined
    length: 2
]

四、Buffer的拷贝

if (isBuffer(value)) {
  return cloneBuffer(value, isDeep)
}
const Buffer = moduleExports ? root.Buffer : undefined, allocUnsafe = Buffer ? Buffer.allocUnsafe : undefined

function cloneBuffer(buffer, isDeep) {
  if (isDeep) {
    return buffer.slice()
  }
  const length = buffer.length
  const result = allocUnsafe ? allocUnsafe(length) : new buffer.constructor(length)

  buffer.copy(result)
  return result
}

以上对buffer对象相关的一些引用作处理。Buffer.allocUnsafe() 在node中返回指定大小的新未初始化Buffer实例。

具体能够参考:Buffer.allocUnsafe

五、Object的拷贝

Object的拷贝开始,会使用Object.create()构造出一个空对象,用以实现原对象的原型继承。

// 用来检测value是否为原型对象
function isPrototype(value) {
  const Ctor = value && value.constructor
  const proto = (typeof Ctor === 'function' && Ctor.prototype) || objectProto

  return value === proto
}

function initCloneObject(object) {
  return (typeof object.constructor === 'function' && !isPrototype(object))
    ? Object.create(Object.getPrototypeOf(object))
    : {}
}
4.一、使用数据缓存来维护对象的拷贝
stack || (stack = new Stack)
const stacked = stack.get(value)
if (stacked) {
return stacked
}
// 这里的result是上面一系列代码生成的初始化对象,能够暂时把它理解为一个包含原型继承关系的空对象
stack.set(value, result)

上面代码创建了Stack,这是个数据管理接口,将子对象的值做为key-value一对一的形式缓存起来,其内部详细的缓存行为大概细分为HashCacheMapCacheListCache,为何使用三种对象缓存策略?

HashCache本质上是用对象的存储方式,但是会有个限制,js中的对象存储,本质上是键值对的集合(Hash 结构),只能限制使用字符串/Symbol看成键,这给它的使用带来了很大的限制。而Map提供了一种更完善的 Hash 结构实现,它的key能够是各类类型,因此在keyObject/Array等类型的场景下,lodash内部使用了MapCache

4.1.一、Stack
class Stack{
    ...
    const LARGE_ARRAY_SIZE = 200
    // Stack的set方法
    set(key, value) {
        let data = this.__data__
        if (data instanceof ListCache) {
          const pairs = data.__data__
          if (pairs.length < LARGE_ARRAY_SIZE - 1) {
            pairs.push([key, value])
            this.size = ++data.size
            return this
          }
          data = this.__data__ = new MapCache(pairs)
        }
        data.set(key, value)
        this.size = data.size
        return this
    }
    ...
}

Stack的入口逻辑能够看到,当缓存内部__data__的长度超出LARGE_ARRAY_SIZE限额时,构造了MapCache的实例,并采用了MapCache的内部set方法,不然使用ListCache

4.1.二、LstCache

ListCache实际上是一个二维数组类型的数据结构

class ListCache {
    ...
    // ListCache中set方法,实现了二维数组式存储
    set(key, value) {
        const data = this.__data__
        const index = assocIndexOf(data, key)
    
        if (index < 0) {
          ++this.size
          data.push([key, value])
        } else {
          data[index][1] = value
        }
        return this
    }
    ...
}
4.1.三、MapCache

下面是MapCache的存储主要实现:

// 初始化数据结构
this.__data__ = {
  'hash': new Hash,
  'map': new Map,
  'string': new Hash
}

set(key, value) {
    const data = getMapData(this, key)
    const size = data.size
    data.set(key, value)
    this.size += data.size == size ? 0 : 1
    return this
}

// 根据key的类型来判断该数据的存储方式,Hash或者Map
function getMapData({ __data__ }, key) {
  const data = __data__
  return isKeyable(key)
    ? data[typeof key === 'string' ? 'string' : 'hash']
    : data.map
}

// 检查 value 是否适合用做惟一对象键
function isKeyable(value) {
  const type = typeof value
  return (type === 'string' || type === 'number' || type === 'symbol' || type === 'boolean')
    ? (value !== '__proto__')
    : (value === null)
}
4.1.四、HashCache

有下面的代码能够看出,Hash 实际上是用对象形式作缓存

const HASH_UNDEFINED = '__lodash_hash_undefined__'

this.__data__ = Object.create(null)

set(key, value) {
    const data = this.__data__
    this.size += this.has(key) ? 0 : 1
    data[key] = value === undefined ? HASH_UNDEFINED : value
    return this
}
4.二、循环引用问题
const loopObject = { a: 1 }
loopObject.b = loopObject

🌰中,loopObject中的b就是一个循环引用的属性。

因为这个特殊状况的存在,在使用JSON.parse(JSON.stringify(loopObject))时会出现内存溢出的问题。

使用缓存的另外一个好处是,可以处理对象中循环引用的状况。在遍历到循环引用对象时,缓存策略会从ceche中利用对应的key找出对应的value,若是对应的引用已经拷贝了,就不须要在再次执行拷贝了,避免了溢出的问题。

五、递归拷贝

if (tag == mapTag) {
    value.forEach((subValue, key) => {
      result.set(key, baseClone(subValue, bitmask, customizer, key, value, stack))
    })
    return result
}
// 当前是set类型
if (tag == setTag) {
    value.forEach((subValue) => {
      result.add(baseClone(subValue, bitmask, customizer, subValue, value, stack))
    })
    return result
}

// 其余的可迭代对象,好比Array/Object
arrayEach(props || value, (subValue, key) => {
    if (props) {
      key = subValue
      subValue = value[key]
    }
    // 递归进行数据的克隆
    assignValue(result, key, baseClone(subValue, bitmask, customizer, key, value, stack))
})

字对象的递归拷贝主要递归使用了baseClone(),并对不一样类型的对象做区分。

5、小结

以上是对lodash的深拷贝作了一个大概流程的分析,并无具体到每个函数实现,特别是Stack中几种缓存结构的深刻解析,之后会持续更新HashCache、MapCache和ListCache相关内容。

🦉🦉🦉

---------------NO WAN---------------

相关文章
相关标签/搜索