• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

loadsh的深拷贝的探究

武飞扬头像
lixin
帮助1

什么是深拷贝

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

深拷贝使用的场景

JSON.parse(JSON.stringify(object))

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

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

自己实现一个深拷贝

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

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

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 的方法。

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)

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 做额外处理。

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]
 toString.call(undefined); // [object Undefined]
 toString.call(null); // [object Null]
 toString.call(argument); // [object Arguments]

3.数组和正则的拷贝

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() 方法返回一个数组(包含额外的属性 index 和 input

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.处理对象和函数

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

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

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

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
}

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

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.递归拷贝

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.循环引用

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

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

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

7.Map 和 Set

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

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

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

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

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

8.Symbol 和 原型链

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 完整代码

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

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是如何实现深拷贝的

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhgahbai
系列文章
更多 icon
同类精品
更多 icon
继续加载