关于loadsh中的深拷贝的探究

什么是深拷贝

深拷贝是指拷贝所有的属性,并且拷贝属性指向的动态分配的内存。当对象和它所引用的对象一起拷贝时即发生深拷贝。深拷贝相比于浅拷贝速度较慢并且花销较大。拷贝前后两个对象互不影响。

深拷贝使用的场景

JSON.parse(JSON.stringify(object))

在大多数情况下,这种方式都是完全可以,但是这样使用也有很大的缺陷:

  • 会忽略 undefined、symbol、函数
  • 不能解决循环引用的对象
  • 不能正确处理 new Date()
  • 不能处理正则

自己实现一个深拷贝

当数据的复杂程度出现到JSON.stringify()不能完全处理的时候,可以选择自制utils工具包来把深拷贝做成一个工具来调用,而要使得深拷贝的功能完全,势必要解决上面出现的问题。

其实深拷贝可以拆分成 2 步,浅拷贝 + 递归,浅拷贝时判断属性值是否是对象,如果是对象就进行递归操作,两个一结合就实现了深拷贝

js 复制代码
function cloneDeep(source) {
    var target = {};
    for(var key in source) {
        if (Object.prototype.hasOwnProperty.call(source, key)) {
            if (typeof source[key] === 'object') {
                target[key] = cloneDeep1(source[key]); // 注意这里
            } else {
                target[key] = source[key];
            }
        }
    }
    return target;
}

这段代码中的关键点是使用 Object.prototype.hasOwnProperty.call(source, key) 来检查是否是 source 对象自身的属性,而不是继承自原型链的属性。这样可以避免复制继承自原型的属性。

这就是一个简单的深拷贝,但是仍然存在较大的问题

  • 没有考虑数组的写法
  • 对对象的判断逻辑不严谨,因为typeof null === object
  • 没有对传入参数校验,比如传入null 应该返回 null 而不是 {}

接下来就是对这些缺陷进行修复,具体参考这篇文章。但在实际的工作过程中,还是使用第三方库loadsh更多,所以就想要去看看loadsh中的深拷贝是如何实现的,这里是loadsh中深拷贝的源码

接下来是自己学习源码的过程,以及自己的学习笔记:

1. 位掩码

入口文件是 cloneDeep.js,直接调用核心文件 baseClone.js 的方法。

js 复制代码
import baseClone from './.internal/baseClone.js'

const CLONE_DEEP_FLAG = 1
const CLONE_SYMBOLS_FLAG = 4

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

export default cloneDeep

第一个参数是需要拷贝的对象,第二个是位掩码(Bitwise)

js 复制代码
function baseClone(value, bitmask, customizer, key, object, stack) {
    // 其他代码
    ......
}

先介绍下该方法的参数 baseClone(value, bitmask, customizer, key, object, stack)

  • value:需要拷贝的对象
  • bitmask:位掩码,其中 1 是深拷贝,2 拷贝原型链上的属性,4 是拷贝 Symbols 属性
  • customizer:定制的 clone 函数
  • key:传入 value 值的 key
  • object:传入 value 值的父对象
  • stack:Stack 栈,用来处理循环引用

位掩码用于处理同时存在多个布尔选项的情况,其中掩码中的每个选项的值都等于 2 的幂 。相比直接使用变量来说,优点是可以节省内存(1/32)(来自MDN

2.标记值的类型

这一步骤主要是来判断具体类型,以便后续根据具体类型来对不同的数据进行不同的深拷贝操作。其中比较关键的是对JavaScript的一个历史遗留问题,用的基本方法是 typeof,但是因为 typeof null 的值也是 'object',所以最后的 return 需要对 null 做额外处理。

js 复制代码
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对应的具体类型:

js 复制代码
 var toString = Object.prototype.toString;
 toString.call(new Date); // [object Date]
 toString.call(new String); // [object String]
 toString.call(Math); // [object Math]
 toString.call(undefined); // [object Undefined]
 toString.call(null); // [object Null]
 toString.call(argument); // [object Arguments]

3.数组和正则的拷贝

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

js 复制代码
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
]

4.处理对象和函数

js 复制代码
const isArr = Array.isArray(value)
const tag = getTag(value)
if (isArr) {
    ... // 数组情况,详见上面解析
} else {
    // 函数
    const isFunc = typeof value == 'function'

    // 如果是 Buffer 对象,拷贝并返回
    if (isBuffer(value)) {
        return cloneBuffer(value, isDeep)
    }
    
    // Object 对象、类数组、或者是函数但没有父对象
    if (tag == objectTag || tag == argsTag || (isFunc && !object)) {
        // 拷贝原型链或者 value 是函数时,返回 {},不然初始化对象
        result = (isFlat || isFunc) ? {} : initCloneObject(value)
        if (!isDeep) {
            return isFlat
                ? copySymbolsIn(value, copyObject(value, keysIn(value), result))
            	: copySymbols(value, Object.assign(result, value))
        }
    } else {
        // 在 cloneableTags 中,只有 error 和 weakmap 返回 false
        // 函数或者 error 或者 weakmap 时,
        if (isFunc || !cloneableTags[tag]) {
            // 存在父对象返回value,不然返回空对象 {}
            return object ? value : {}
        }
        // 初始化非常规类型
        result = initCloneByTag(value, tag, isDeep)
    }
}

通过上面代码可以发现,函数、errorweakmap 时返回空对象 {},并不会真正拷贝函数。

value 类型是 Object 对象和类数组时,调用 initCloneObject 初始化对象,最终调用 Object.create 生成新对象。

js 复制代码
function initCloneObject(object) {
    // 构造函数并且自己不在自己的原型链上
    return (typeof object.constructor == 'function' && !isPrototype(object))
        ? Object.create(Object.getPrototypeOf(object))
    	: {}
}

// 本质上实现了一个instanceof,用来测试自己是否在自己的原型链上
function isPrototype(value) {
    const Ctor = value && value.constructor
    // 寻找对应原型
    const proto = (typeof Ctor == 'function' && Ctor.prototype) || Object.prototype
    return value === proto
}

对于非常规类型对象,通过各自类型分别进行初始化。

js 复制代码
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: // Map 类型
            return new Ctor

        case numberTag: // 数字和字符串类型
        case stringTag:
            return new Ctor(object)

        case regexpTag: // 正则
            return cloneRegExp(object)

        case setTag: // Set 类型
            return new Ctor

        case symbolTag: // Symbol 类型
            return cloneSymbol(object)
    }
}

5.递归拷贝

js 复制代码
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(),并对不同类型的对象作区分。

6.循环引用

构造了一个栈用来解决循环引用的问题。

js 复制代码
// 主线代码
stack || (stack = new Stack)
const stacked = stack.get(value)
// 已存在
if (stacked) {
    return stacked
}
stack.set(value, result)

如果当前需要拷贝的值已存在于栈中,说明有环,直接返回即可。栈中没有该值时保存到栈中,传入 valueresult。这里的 result 是一个对象引用,后续对 result 的修改也会反应到栈中。

7.Map 和 Set

value 值是 Map 类型时,遍历 value 并递归其 subValue,遍历完成返回 result 结果。

js 复制代码
// 主线代码
if (tag == mapTag) {
    value.forEach((subValue, key) => {
        result.set(key, baseClone(subValue, bitmask, customizer, key, value, stack))
    })
    return result
}

value 值是 Set 类型时,遍历 value 并递归其 subValue,遍历完成返回 result 结果。

js 复制代码
// 主线代码
if (tag == setTag) {
    value.forEach((subValue) => {
        result.add(baseClone(subValue, bitmask, customizer, subValue, value, stack))
    })
    return result
}

上面的区别在于添加元素的 API 不同,即 Map.setSet.add

8.Symbol 和 原型链

js 复制代码
const symbolValueOf = Symbol.prototype.valueOf
function cloneSymbol(symbol) {
    return Object(symbolValueOf.call(symbol))
}

首先获取到 Symbol.prototype.valueOf 方法,并且使用 call 方法将该方法应用到传入的 symbol 参数上,从而获取到原始 symbol 值。然后通过 Object 构造函数将原始 symbol 值包装成对象进行返回。通过这种方式,就可以克隆一个 Symbol 类型的值。需要注意的是,使用 Object 构造函数进行包装后,返回的仍然是一个新的对象,不同于原始的 symbol 值。

baseClone 完整代码

这部分就是核心代码了,各功能分割如下,详细功能实现部分将对各个功能详细解读。

js 复制代码
function baseClone(value, bitmask, customizer, key, object, stack) {
    let result

    // 标志位
    const isDeep = bitmask & CLONE_DEEP_FLAG		// 深拷贝,true
    const isFlat = bitmask & CLONE_FLAT_FLAG		// 拷贝原型链,false
    const isFull = bitmask & CLONE_SYMBOLS_FLAG	// 拷贝 Symbol,true

    // 自定义 clone 函数
    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)
    const tag = getTag(value)
    if (isArr) {
        // 数组
        result = initCloneArray(value)
        if (!isDeep) {
            return copyArray(value, result)
        }
    } else {
        // 对象
        const isFunc = typeof value == 'function'

        if (isBuffer(value)) {
            return cloneBuffer(value, isDeep)
        }
        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)
        }
    }
    // 循环引用
    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
    }

    // TypedArray
    if (isTypedArray(value)) {
        return result
    }

    // Symbol & 原型链
    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))
    })
    
    // 返回结果
    return result
}

参考文章

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

「读懂源码系列3」lodash 是如何实现深拷贝的(上)

【进阶4-4期】Lodash是如何实现深拷贝的

相关推荐
道不尽世间的沧桑1 小时前
第17篇:网络请求与Axios集成
开发语言·前端·javascript
diemeng11192 小时前
AI前端开发技能变革时代:效率与创新的新范式
前端·人工智能
bin91534 小时前
DeepSeek 助力 Vue 开发:打造丝滑的复制到剪贴板(Copy to Clipboard)
前端·javascript·vue.js·ecmascript·deepseek
晴空万里藏片云5 小时前
elment Table多级表头固定列后,合计行错位显示问题解决
前端·javascript·vue.js
曦月合一6 小时前
html中iframe标签 隐藏滚动条
前端·html·iframe
奶球不是球6 小时前
el-button按钮的loading状态设置
前端·javascript
kidding7236 小时前
前端VUE3的面试题
前端·typescript·compositionapi·fragment·teleport·suspense
无责任此方_修行中7 小时前
每周见闻分享:杂谈AI取代程序员
javascript·资讯
Σίσυφος19008 小时前
halcon 条形码、二维码识别、opencv识别
前端·数据库
学代码的小前端8 小时前
0基础学前端-----CSS DAY13
前端·css