likes
comments
collection
share

扒一扒Lodash 中的深拷贝,学问可真不少 | 【javascript基础系列】| 源码解读

作者站长头像
站长
· 阅读数 20

前言

本文是在学习 Loash源码的时候做的笔记和总结 深拷贝,这个知识点主要涉及到 JS的类型,类型判断 等 这些知识点 我们平时可能很多时候用 JSON.parse(JSON.stringify(xx)) 来实现简单的深拷贝,但是会存在很大的问题 看上去知识点很简单,但是真的简单吗?其实细节很多,一点都不简单

JS 中深拷贝的痛点在哪里

  • JS中的数据类型比较多,需要一个个判断,所以,如何有效的区分每个细分的数据类型
  • 如何深拷贝每种数据类型
  • 面对循环引用问题,如何解决

源码中文备注

// clone  - baseClone(value, CLONE_SYMBOLS_FLAG)  浅拷贝
// cloneWith -  baseClone(value, CLONE_SYMBOLS_FLAG, customizer) 浅拷贝 customizer 接受一个函数自定义拷贝的函数(如果这个函数返回undefined,那么按照正常的逻辑来走)
// cloneDeep -  baseClone(value, CLONE_DEEP_FLAG | CLONE_SYMBOLS_FLAG)  深拷贝 ,会递归的拷贝
// cloneDeepWith  -   baseClone(value, CLONE_DEEP_FLAG | CLONE_SYMBOLS_FLAG, customizer) 深拷贝,也可以接受 自定义的拷贝函数 customizer

/**
 * The base implementation of `clone` and `cloneDeep` which tracks
 * traversed objects.
 *
 * @private
 * @param {*} value The value to clone. 需要拷贝的值
 * @param {number} bitmask The bitmask flags.  配置值 
 *  1 - Deep clone  1.深拷贝
 *  2 - Flatten inherited properties 
 *  4 - Clone symbols
 * @param {Function} [customizer] The function to customize cloning. 自定义拷贝函数
 * @param {string} [key] The key of `value`. 需要拷贝的值 对应的 key
 * @param {Object} [object] The parent object of `value`. 需要拷贝的值
 * @param {Object} [stack] Tracks traversed objects and their clone counterparts. 将每个需要拷贝的值,存进缓存,主要是为了解决 循环引用问题
 * @returns {*} Returns the cloned value.
 */
function baseClone(value, bitmask, customizer, key, object, stack) {
  let result
  // bitmask 通过这一个配置的值,决定 下面 isDeep isFlat isFull 这三个配置值
  // & 位操作符号,通过一个值与不同的值进行 位操作,能决定出多个个不同的值!  
  // & 位操作符的优点是:节省内存,但是缺点也很明显:不直观。一般不建议用,直接 设置多个参数值即可
  const isDeep = bitmask & CLONE_DEEP_FLAG  // 是否为深拷贝(false 即浅拷贝)
  const isFlat = bitmask & CLONE_FLAT_FLAG // 是否展平 : 决定是否将对象的原型上的属性也拷贝
  const isFull = bitmask & CLONE_SYMBOLS_FLAG //是否是全部的key:决定是否将 symbol的的 key 也深拷贝

  // 深拷贝具体对应的配置是 isDeep = true, isFlat = false , isFull = true

  // 如果有自定义 拷贝函数
  if (customizer) {
    result = object ? customizer(value, key, object, stack) : customizer(value)
  }
  // 如果自定义的 拷贝方法 可以返回值,那么直接返回即可
  if (result !== undefined) {
    return result
  }
  // 如果是 基本类型,那么直接返回 值即可
  if (!isObject(value)) {
    return value
  } 
  const isArr = Array.isArray(value)
  // 此处的tag 是   Object.prototype.toString.call(value) 的结果 [object Array] 等
  const tag = getTag(value)
  // 如果是 数据单独处理
  if (isArr) {
    // 只是初始化 数组
    result = initCloneArray(value)
    if (!isDeep) {
      // 浅拷贝 - 直接拷贝返回了
      return copyArray(value, result)
    }
  } else {
    // 非数组的逻辑
    const isFunc = typeof value === 'function'
    // 如果是 Buffer
    if (isBuffer(value)) {
      // buffer的拷贝
      return cloneBuffer(value, isDeep)
    }
    // 如果是 Object 或则 Arguments 或则 function(顶层函数,也就是第一层的函数)
    if (tag == objectTag || tag == argsTag || (isFunc && !object)) {
      // 初始化 Object 
      // 注意:如果 isFlat(key展平) 或则是 函数(顶层函数),那么只是 初始化为 {}
      result = (isFlat || isFunc) ? {} : initCloneObject(value)
      if (!isDeep) {
        // 浅拷贝 isFlat : 是否展平 - 是否获取到该对象的原型链上的属性的key
        // 如果是深拷贝:根本不会进入此代码块
        return isFlat
          ? copySymbolsIn(value, copyObject(value, keysIn(value), result))
          : copySymbols(value, Object.assign(result, value))
      }
    } else {
      // 如果是 function(此处为 非顶层的 function,也就从第二层开始往后的所有的 function),那么就直接返回函数,也就是 函数除了第一层的函数,其余的都是 浅拷贝
      // 如果是 Loash 库 不支持的 类型,那么直接返回其值
      if (isFunc || !cloneableTags[tag]) {
        return object ? value : {}
      }
      // 初始化其他类型
      result = initCloneByTag(value, tag, isDeep)
    }
  }
  // 循环引用的问题
  // Check for circular references and return its corresponding clone.
  // 解决循环引用问题,用缓存把每个值都存下来,如果缓存有,肯定是循环引用,直接从缓存中取值即可
  stack || (stack = new Stack)
  const stacked = stack.get(value)
  if (stacked) {
    return stacked
  }
  stack.set(value, result)

  // Map 类型的拷贝
  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
  }
  // TODO 类数组类型的处理
  // 如果是 类型化数组,那么直接返回
  // https://zhuanlan.zhihu.com/p/258353637  详细的解析
  if (isTypedArray(value)) {
    return result
  }
  // 获取所有的 key,以便下面循环 拷贝用
  // 深拷贝,此处  const keysFunc = getAllKeys
  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]
    }
    // Recursively populate clone (susceptible to call stack limits).
    // 递归拷贝(可能会收到 stack 的限制,也就是我们之前在上面写到的 stack 的限制为200)
    assignValue(result, key, baseClone(subValue, bitmask, customizer, key, value, stack))
  })
  return result
}

Lodash 中的深拷贝是如何实现的

Lodash 深拷贝 流程梳理

确定配置参数

  • bitmask:配置标志,主要是用于 位操作的,根据传入的值,可以得到下面的几个配置的具体参数
    • isDeep:是否要深拷贝,一般调用 cloneDeep 方法,此处为 true
    • isFlat:是否要将 Object里面的属性拉平,深拷贝此处为 false
    • isFull: 是否全部属性都要遍历,主要是symbol类型的属性,深拷贝此处为 true
  • customizer: 自定义的拷贝方法,_.cloneDeepWith(value, [customizer]) 主要用在这个方法里面,也就是自己可以自定义对某些特殊的地方自己定义拷贝函数;此处分析我们不考虑这个
  • key:value对应的key,主要是递归调用的时候,递归里面的每个项
  • object:value的父数据,valueobjectkey的值,一样也是 递归的时候用到的
  • stack:这个对象,主要用于判断当前对象是不是循环引用

自定义拷贝函数

如果customizer 有值,那么直接调用customizer函数,得到结果,如果有结果,那么直接返回结果即可,如果没有结果,相当于 customizer没有作用,继续执行下面的代码

注意:customizer在进行递归调用的过程中一直是起作用的

  // 如果有自定义 拷贝函数
  if (customizer) {
    result = object ? customizer(value, key, object, stack) : customizer(value)
  }
  // 如果自定义的 拷贝方法 可以返回值,那么直接返回即可
  if (result !== undefined) {
    return result
  }

如果是基本类型,直接返回

这个比较简单,如果不是引用类型,那么直接返回

function isObject(value) {
  const type = typeof value
  return value != null && (type === 'object' || type === 'function')
}
  // 如果不是引用类型,也就是 基本类型,那么直接返回 值即可
  if (!isObject(value)) {
    return value
  } 

根据不同的数据类型,初始化一个对象,后面用于深拷贝

  • 如果是Array,初始化一个空的Array,以便后面使用
  • 如果是 Buffer类型,那么直接复制即可
const 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
}
  • 如果是 Object,Arguments,function(顶层,而不是递归里面的function)
  • 否则就是初始化 对应的 类型
    // 如果是 Object 或则 Arguments 或则 function(顶层函数,也就是第一层的函数)
    if (tag == objectTag || tag == argsTag || (isFunc && !object)) {
      // 初始化 Object 
      // 注意:如果 isFlat(key展平) 或则是 函数(顶层函数),那么只是 初始化为 {}
      result = (isFlat || isFunc) ? {} : initCloneObject(value)
      if (!isDeep) {
        // 浅拷贝 isFlat : 是否展平 - 是否获取到该对象的原型链上的属性的key
        // 如果是深拷贝:根本不会进入此代码块
        return isFlat
          ? copySymbolsIn(value, copyObject(value, keysIn(value), result))
          : copySymbols(value, Object.assign(result, value))
      }
    } else {
      // 如果是 function(此处为 非顶层的 function,也就从第二层开始往后的所有的 function),那么就直接返回函数,也就是 函数除了第一层的函数,其余的都是 浅拷贝
      // 如果是 Loash 库 不支持的 类型,那么直接返回其值
      if (isFunc || !cloneableTags[tag]) {
        return object ? value : {}
      }
      // 初始化其他类型
      result = initCloneByTag(value, tag, isDeep)
    }

注意:下面代码是根据深拷贝,做了一定的代码删除,因为源码里面此处是公共代码,有其他的逻辑(比如浅拷贝)

//简化后的代码
// 2. 如果是 数组 - 初始化数组
if (isArr) {
    // 只是初始化 数组 - 为啥要初始化?
    result = initCloneArray(value)
  }else{
    // 3. 如果是 Buffer
    if (isBuffer(value)) {
        // buffer的拷贝
        return cloneBuffer(value, isDeep)
      }
      if (tag == objectTag || tag == argsTag || (isFunc && !object)) {
        // 初始化 Object
        result = (isFlat || isFunc) ? {} : initCloneObject(value)
      }else{
        if (isFunc || !cloneableTags[tag]) {
            return object ? value : {}
          }
        result = initCloneByTag(value, tag, isDeep)
      }
  }

循环引用的问题

直接用一个Map,如果是循环引用,直接就返回了

//   循环引用问题
  stack || (stack = new Stack)
  const stacked = stack.get(value)
  if (stacked) {
    return stacked
  }
  stack.set(value, result)

map类型深拷贝

  if (tag == setTag) {
    value.forEach((subValue) => {
      result.add(baseClone(subValue, bitmask, customizer, subValue, value, stack))
    })
    return result
  }

数组类型和其他可迭代类型的处理

  const props = isArr ? undefined : keysFunc(value)
  // 循环深拷贝每个属性,里面会递归
  arrayEach(props || value, (subValue, key) => {
    if (props) {
      key = subValue
      subValue = value[key]
    }
    // Recursively populate clone (susceptible to call stack limits).
    // 递归拷贝(可能会收到 stack 的限制,也就是我们之前在上面写到的 stack 的限制为200)
    assignValue(result, key, baseClone(subValue, bitmask, customizer, key, value, stack))
  })

特别注意点

关于函数的复制

  • 第一层的函数是会被深拷贝的
  • 再往下层递归的话,就没有深拷贝了,而是直接复制的函数的引用,也就是说 原来的函数改了,那么此处的函数也改了
      // 如果是 function(此处为 非顶层的 function,也就从第二层开始往后的所有的 function),那么就直接返回函数,也就是 函数除了第一层的函数,其余的都是 浅拷贝
      // 如果是 Loash 库 不支持的 类型,那么直接返回其值
      if (isFunc || !cloneableTags[tag]) {
        return object ? value : {}
      }
  • 案例测试如下
    let obj_1 = {
        p1_fun:function(){
            console.log('p1 ====')
        },
        p2:{
            name:'p2',
            p2_fun:function(){
                console.log('p2 =========')
            }
        }
    }
    const obj_2 =  _.cloneDeep(obj_1)
    obj_1.p1_fun = function(){
        console.log('p1_fun --- 改了')
    }
    obj_1.p2.p2_fun = function(){
        console.log('p2_fun --- 改了')
    }
    // 此处的func 是深拷贝,不会随着 obj1的改变而改变
    console.log('obj_2.p1=====',obj_2.p1_fun())
    // 此处的func 是引用,随着 obj1 里面的改变而改变
    console.log('obj_2.p2.p2_fun=====',obj_1.p2.p2_fun())
    

对于一些不在范围内的类型(自定义的类型)

如果是一些不支持的类型,那么直接就返回了

知识点拆解(重点)

类型的判断

类型判断的方法有很多,比如typeof,instanceof等,但是都无法适用于全部的场景,而 Object.protoType.toString.call 可以算是比较终极的解法

注意:

  • 源码中基本类型直接用 typeof来判断的(其实也可以直接全部用toString
  • 一定要用 Object上的原型方法,而不是 对象实例的 toString方法,因为每个实例对象都有可能自己实现了自己的实例方法,导致不准确
  • 返回的值是 [Object Date]这样的字符串

关于数据类型判断,详细的可以看这篇文章 你真的会判断javascript中的数据类型吗? | 【javascript基础系列】

关于原生js中的类型属性有多少个,我们来看看Loash里面支持多少类型的深拷贝!

真实情况只会比这个更多(下面的不包含基本类型

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]'

初始化 Object

先看下对应的源码

// 初始化Object,为后面拷贝做准备
function initCloneObject(object) {
  return (typeof object.constructor === 'function' && !isPrototype(object))
    ? Object.create(Object.getPrototypeOf(object))
    : {}
}
// 判断是否是 JS 内部的原型
function isPrototype(value) {
  const Ctor = value && value.constructor
  const proto = (typeof Ctor === 'function' && Ctor.prototype) || objectProto

  // Ctor.prototype 其实就是 拿的 value的原型  [value.constructor.prototype === valle.__proto__]
  // 只是 __proto__ 这种写法 不是标准所支持的

  return value === proto
}

解析

  • 如果此处的值为JS里面原生的对象类型,比如Object,那么初始化比较简单就是 {}
  • 如果此处的值不是 JS里面的原生的对象类型,比如Vue,那么就用此值的构造函数来初始化
  • 判断当前值是否为 Object这样的类型,也就是 源码中 isPrototype方法
  • 获取一个值的构造函数 Object.getPrototypeOf(object)
  • 通过一个动态的构造函数 初始化一个值 Object.create(构造函数),连在一起就可以这样写 Object.create(Object.getPrototypeOf(object))

初始化 Array

先看对应源码

function initCloneArray(array) {
  const { length } = array
  const result = new array.constructor(length)
  // Add properties assigned by `RegExp#exec`.
  // 比较特殊的一个数组情况,也就是 通过正则产生的,会存在 index 和 input的两个属性
  if (length && typeof array[0] === 'string' && hasOwnProperty.call(array, 'index')) {
    result.index = array.index
    result.input = array.input
  }
  return result
}

解析

初始化 array 能理解,但是 后面的是 index,input 是什么鬼?

通过正则产生的数组里面就可能存在这几个参数,所以必须考虑这个情况,下面的例子就完整的演示了这种情况

详细文档地址


var str = 'For more information, see Chapter 3.4.5.1';
var re = /see (chapter \d+(.\d)*)/i;
var found = str.match(re);

console.log(found);

// logs [ 'see Chapter 3.4.5.1',
//        'Chapter 3.4.5.1',
//        '.1',
//        index: 22,
//        input: 'For more information, see Chapter 3.4.5.1' ]

// 'see Chapter 3.4.5.1' 是整个匹配。
// 'Chapter 3.4.5.1''(chapter \d+(.\d)*)'捕获。
// '.1' 是被'(.\d)'捕获的最后一个值。
// 'index' 属性 (22) 是整个匹配从零开始的索引。
// 'input' 属性是被解析的原始字符串。

其他类型初始化

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)
  }
}

循环引用如何解决

先看源码

  // 循环引用的问题
  // Check for circular references and return its corresponding clone.
  stack || (stack = new Stack)
  const stacked = stack.get(value)
  if (stacked) {
    return stacked
  }
  stack.set(value, result)

解析

其实也很简单,就是用一个 Set做一个字典映射,只要是 引用类型,就直接用此处的值 作为 key存进 缓存中

每次遇到引用的值,都直接去缓存中查看下,有的话,直接赋值即可,如果缓存中没有,那么就存进缓存

这样就解决了循环引用的问题

有个小问题:

就是缓存的存储数据的个数有大小限制 200 个

获取所有的Key

Map的拷贝

  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
  }

总结

Loash是一个经典,并且经过社区检验的过的一个第单方库,通过对其 深拷贝 函数的源码解读,我们学到了很多

也不经感叹:就一个小小的深拷贝,涉及的知识点和细节真多

这是一个源码阅读系列文章,不断更新,希望对大家有帮助

有问题可以留言一起探讨