什么是深拷贝
深拷贝是指拷贝所有的属性,并且拷贝属性指向的动态分配的内存。当对象和它所引用的对象一起拷贝时即发生深拷贝。深拷贝相比于浅拷贝速度较慢并且花销较大。拷贝前后两个对象互不影响。
深拷贝使用的场景
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')
?index
和input
是什么情况?
熟悉js正则匹配的会知道,这里考虑了一种特殊的数组情况,那就是regexObj.exec(str)
,用来处理匹配正则时,执行exec()
的返回结果情况,如果匹配成功,exec()
方法返回一个数组(包含额外的属性 index
和 input
)
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)
}
}
通过上面代码可以发现,函数、error
和 weakmap
时返回空对象 {},并不会真正拷贝函数。
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)
如果当前需要拷贝的值已存在于栈中,说明有环,直接返回即可。栈中没有该值时保存到栈中,传入 value
和 result
。这里的 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.set
和 Set.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
}