深拷贝
本文将从最基础的深拷贝方法到更复杂的方法,深入讲解深拷贝的原理,以及各方法之间的差异。
JSON.parse(JSON.stringify())
在不使用第三方库的情况下,想要深拷贝一个对象,一般来讲最简单的用的最多的就是 JSON.parse(JSON.stringify(obj))
,其过程说白了就是利用 JSON.stringify
将 JS 对象序列化(JSON字符串),再使用 JSON.parse
来反序列化(还原) JS 对象。
JSON.parse(JSON.stringify(obj));
这种写法非常简单,而且可以应对大部分的应用场景,但注意 JSON 只能用来序列化对象、数组、数值、字符串、布尔值和 null
,依靠 JSON 深拷贝时存在很大缺陷,原因在于 JSON.stringify()
在序列化时会有以下问题:
1、时间对象序列化后会变成字符串;
const target = {
name: 'Jack',
date: [new Date(1536627600000), new Date(1540047600000)]
};
JSON.parse(JSON.stringify(target));
Date 日期调用了 toJSON() 将其转换为了 string 字符串(同 Date.toISOString()),因此会被当做字符串处理。
JSON.stringify(new Date(1536627600000));
// '"2018-09-11T01:00:00.000Z"'
2、RegExp、Error 对象序列化后将只得到空对象;
const target = {
re: new RegExp("\\w+"),
err: new Error('"x" is not defined')
};
JSON.stringify(target);
// '{"re":{},"err":{}}'
3、任意的函数、undefined
以及 symbol 值,在序列化过程中会被忽略;
const target = {
func: function () {
console.log(1)
},
val: undefined,
sym: Symbol('foo')
};
JSON.stringify(target);
// '{}'
4、NaN 和 Infinity 格式的数值都会被当做 null;
1.7976931348623157E+10308
是浮点数的最大上限 显示为 Infinity-1.7976931348623157E+10308
是浮点数的最小下限 显示为 -Infinity
const target = {
nan: NaN,
infinityMax: 1.7976931348623157E+10308,
infinityMin: -1.7976931348623157E+10308,
};
JSON.stringify(target);
// '{"nan":null,"infinityMax":null,"infinityMin":null}'
5、对包含循环引用的对象(对象之间相互引用,形成无限循环)序列化,会抛出错误。
var circularReference = { otherData: 123 };
circularReference.myself = circularReference;
JSON.stringify(circularReference);
// TypeError: cyclic object value(Firefox) 或 Uncaught TypeError: Converting circular structure to JSON(Chrome and Opera)
在 JSON 中出现循环引用时,JavaScript 会抛出 "cyclic object value" 的异常。
JSON.stringify()
并不会尝试解决这个问题,因此导致运行失败。
- 提示信息:
- TypeError: cyclic object value (Firefox)
- TypeError: Converting circular structure to JSON (Chrome and Opera)
- TypeError: Circular reference in value argument not supported (Edge)
递归
通过递归实现深拷贝:
function deepClone(obj) { // 递归拷贝
if (typeof obj !== 'object' || obj === null) return obj; // 如果不是复杂数据类型 或者为null,直接返回
if (obj instanceof RegExp) return new RegExp(obj);
if (obj instanceof Date) return new Date(obj);
let cloneObj = Array.isArray(obj) ? [] : {};
for (let key in obj) {
// 判断是否是对象自身的属性,筛掉对象原型链上继承的属性
if (obj.hasOwnProperty(key)) {
// 如果 obj[key] 是复杂数据类型,递归
cloneObj[key] = deepClone(obj[key]);
}
}
return cloneObj;
}
我们应该拷贝要拷贝对象自身的属性,对象原型上的属性我们不应该拷贝,这里我们用到 hasOwnProperty()
方法来解决。
hasOwnProperty()
方法会返回一个布尔值,这个方法可以用来检测一个对象是否含有特定的自身属性;该方法会忽略掉那些从原型链上继承到的属性。
循环引用
循环引用会使递归进入死循环导致栈内存溢出。
我们拷贝一下前面循环引用的例子:
var circularReference = { otherData: 123 };
circularReference.myself = circularReference;
deepClone(circularReference);
// Uncaught RangeError: Maximum call stack size exceeded 超出最大调用堆栈大小
解决循环引用问题,可以额外开辟一个存储空间,来存储当前对象和拷贝对象的对应关系,当需要拷贝当前对象时,先去存储空间中找,有没有拷贝过这个对象,如果有的话直接返回,如果没有的话继续拷贝,这样就巧妙化解的循环引用的问题。
这个存储空间,需要可以存储 key-value
形式的数据,且 key
可以是一个引用类型,我们可以选择 Map
这种数据结构:
- 检查
map
中有无克隆过的对象 - 有 - 直接返回
- 没有 - 将当前对象作为
key
,克隆对象作为value
进行存储 - 继续克隆
function deepClone(obj, map = new Map()) { // 递归拷贝
if (typeof obj !== 'object' || obj === null) return obj; // 如果不是复杂数据类型 或者为null,直接返回
if (obj instanceof RegExp) return new RegExp(obj);
if (obj instanceof Date) return new Date(obj);
if (map.has(obj)) return map.get(obj);
let cloneObj = Array.isArray(obj) ? [] : {};
map.set(obj, cloneObj);
for (let key in obj) {
// 判断是否是对象自身的属性,筛掉对象原型链上继承的属性
if (obj.hasOwnProperty(key)) {
// 如果 obj[key] 是复杂数据类型,递归
cloneObj[key] = deepClone(obj[key], map);
}
}
return cloneObj;
}
再次执行前面的用例可以发现没有报错,循环引用的问题解决了。
使用 WeakMap 优化
下面我们用 WeakMap
替代 Map
来优化深拷贝的实现。
如下:
function deepClone(obj, map = new WeakMap()) {
// ...
};
为什么要这样做呢?先来看看 WeakMap
的作用:
WeakMap 对象是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值可以是任意的。
什么是弱引用呢?
在计算机程序设计中,弱引用与强引用相对,是指不能确保其引用的对象不会被垃圾回收器回收的引用。 一个对象若只被弱引用所引用,则被认为是不可访问(或弱可访问)的,并因此可能在任何时刻被回收。
我们默认创建一个对象:const obj = {}
,就默认创建了一个强引用的对象,我们只有手动将 obj = null
,它才会被垃圾回收机制进行回收,如果是弱引用对象,垃圾回收机制会自动帮我们回收。
举个例子:
如果我们使用 Map
的话,那么对象间是存在强引用关系的:
let obj = { name : 'Jack'}
const target = new Map();
target.set(obj,'person');
obj = null;
虽然我们手动将 obj
,进行释放,然是 target
依然对 obj
存在强引用关系,所以这部分内存依然无法被释放。
再来看 WeakMap
:
let obj = { name : 'Jack'}
const target = new WeakMap();
target.set(obj,'person');
obj = null;
如果是 WeakMap
的话,target
和 obj
存在的就是弱引用关系,当下一次垃圾回收机制执行时,这块内存就会被释放掉。
设想一下,如果我们要拷贝的对象非常庞大时,使用 Map
会对内存造成非常大的额外消耗,而且我们需要手动清除 Map
的属性才能释放这块内存,而 WeakMap
会帮我们巧妙化解这个问题。
我也经常在某些代码中看到有人使用 WeakMap
来解决循环引用问题,但是解释都是模棱两可的,当你不太了解 WeakMap
的真正作用时。我建议你也不要在面试中写这样的代码,结果只能是给自己挖坑,即使是准备面试,你写的每一行代码也都是需要经过深思熟虑并且非常明白的。
能考虑到循环引用的问题,你已经向面试官展示了你考虑问题的全面性,如果还能用 WeakMap
解决问题,并很明确的向面试官解释这样做的目的,那么你的代码在面试官眼里应该算是合格了。
- 循环引用部分内容出自
ConardLi大佬
如何写出一个惊艳面试官的深拷贝?。
structuredClone()
Window
接口的 structuredClone()
方法使用结构化克隆算法将给定的值进行深拷贝。
该方法还支持把原值中的可转移对象转移(而不是拷贝)到新对象上。可转移对象与原始对象分离并附加到新对象;它们将无法在原始对象中被访问。
结构化克隆算法
结构化克隆算法用于复制复杂 JavaScript 对象的算法。通过来自 Worker的 postMessage()
或使用 IndexedDB 存储对象时在内部使用。它通过递归输入对象来构建克隆,同时保持先前访问过的引用的映射,以避免无限遍历循环。
结构化克隆所不能做到的
Function
对象是不能被结构化克隆算法复制的;如果你尝试这样子去做,这会导致抛出DATA_CLONE_ERR
的异常。- 企图去克隆 DOM 节点同样会抛出
DATA_CLONE_ERR
异常。- 对象的某些特定参数也不会被保留
RegExp
对象的lastIndex
字段不会被保留- 属性描述符,setters 以及 getters(以及其他类似元数据的功能)同样不会被复制。例如,如果一个对象用属性描述符标记为 read-only,它将会被复制为 read-write,因为这是默认的情况下。
- 原形链上的属性也不会被追踪以及复制。
支持的类型
JavaScript 类型
Array
ArrayBuffer
DataView
Date
Error
类型(仅限部分 Error 类型))。Map
Object
对象:仅限简单对象(如使用对象字面量创建的)。- 除
symbol
以外的基本类型。 RegExp
:lastIndex
字段不会被保留。Set
String
TypedArray
不支持的类型
函数
- 函数包含执行环境(闭包上下文),这些环境无法序列化。
- 克隆后无法恢复原始作用域链。
- 函数可能引用外部变量,而这些引用在新环境中无法还原。
DOM元素
- DOM 元素依赖浏览器的渲染上下文。
- 元素包含事件监听器、样式、状态、节点树等复杂结构,不能用普通 JS 表达。
- DOM 是浏览器内部的宿主对象,不是纯 JS 对象。
Proxy
- Proxy 本质是动态行为的代理,它不是数据,而是一种 行为包装器。
- 内部的 handler(代理对象)包含不可见的行为逻辑,structuredClone 无法复原。
- 克隆后即使强行复制,行为可能完全不同。
Error 子类
- Error 对象包含 stack trace(堆栈信息)、message 等与运行时紧密绑定的内容。
- 虽然有的浏览器支持部分
Error
克隆,但并不是规范强制要求。
实际上,所有深拷贝都不能完美解决以上类型。
Lodash.cloneDeep()
Lodash
实现了函数、正则、Date、Buffer、Map、Set、原型链等情况下的深拷贝。
入口
入口文件是 cloneDeep.js
,直接调用核心文件 baseClone.js
的方法。
baseClone
关联了多种 clone 方法,如clone
,cloneWith
,cloneDeep
,cloneDeepWith
等
const CLONE_DEEP_FLAG = 1
const CLONE_SYMBOLS_FLAG = 4
function cloneDeep(value) {
return baseClone(value, CLONE_DEEP_FLAG | CLONE_SYMBOLS_FLAG)
}
第一个参数是需要拷贝的对象,第二个是位掩码(Bitwise),关于位掩码的详细介绍请看下面拓展部分。
baseClone 方法
然后我们进入 baseClone.js
查看具体方法,主要实现逻辑都在这个方法里。
先介绍下该方法的参数 baseClone(value, bitmask, customizer, key, object, stack)
初始化需要的数据:
value
:字符串,数字,对象,数组,函数bitmask
:位掩码,其中 1 是深拷贝,2 拷贝原型链上的属性,4 是拷贝 Symbols 属性customizer
:定制的clone
函数 (这个是给cloneWith
函数用的,与本文无关)递归中需要的数据:
value
:字符串,数字,对象,数组,函数key
:字符串,数字或 Symbolobject
:父对象stack
:Stack 栈,用来处理循环引用customizer
:定制的clone
函数
我将分成以下几部分进行讲解,可以选择自己感兴趣的部分阅读。
- 位掩码
- 定制
clone
函数 - 基本数据类型
- 数组 & 正则
- 对象 & 函数
- 循环引用
- Map & Set
- Symbol & 原型链
位掩码
上面简单介绍了位掩码,参数定义如下。
// 主线代码
const CLONE_DEEP_FLAG = 1 // 1 即 0001,深拷贝标志位
const CLONE_FLAT_FLAG = 2 // 2 即 0010,拷贝原型链标志位,
const CLONE_SYMBOLS_FLAG = 4 // 4 即 0100,拷贝 Symbols 标志位
位掩码用于处理同时存在多个布尔选项的情况,其中掩码中的每个选项的值都等于 2 的幂。相比直接使用变量来说,优点是可以节省内存
人话:原本是用布尔值(true/false)存储状态的,现在用二进制来代替了
js// 位掩码方式 flags = 0b0101; // 表示第0位和第2位为true // 布尔方式 flag1 = true; flag2 = false; flag3 = true;
这就是为什么没有3,因为3是011,有两位1,再后续每位也是 2 的幂,如8为1000
// cloneDeep.js 添加标志位,1 | 4 即 0001 | 0100 即 0101 即 5
CLONE_DEEP_FLAG | CLONE_SYMBOLS_FLAG
// 主线代码
// baseClone.js 取出标志位
let result // 初始化返回结果,后续代码需要,和位掩码无关
const isDeep = bitmask & CLONE_DEEP_FLAG // 5 & 1 即 1 即 true
const isFlat = bitmask & CLONE_FLAT_FLAG // 5 & 2 即 0 即 false
const isFull = bitmask & CLONE_SYMBOLS_FLAG // 5 & 4 即 4 即 true
定制 clone
函数
用于 cloneWith
函数
// 主线代码
// 判断是否传入customizer函数
if (customizer) {
result = object ? customizer(value, key, object, stack) : customizer(value);
}
if (result !== undefined) {
return result;
}
基本数据类型
非对象的就是基本数据类型,判断要拷贝的值是否是对象,非对象直接返回本来的值
function
也是对象
// 主线代码
if (!isObject(value)) {
return value;
}
function isObject(value) {
const type = typeof value;
return value != null && (type == 'object' || type ='function');// 非空 函数 对象
}
判断完基础数据类型后就只剩下数组和对象了
数组 & 正则
// 主线代码
const isArr = Array.isArray(value)
const hasOwnProperty = Object.prototype.hasOwnProperty
// 判断数组
if (isArr) {
result = initCloneArray(value)
if (!isDeep) {
return copyArray(value, result)
}
} else {
// 非数组,后面解析
}
// 初始化一个数组
function initCloneArray(array) {
const { length } = array
// 构造相同长度的新数组
const result = new array.constructor(length)
// 正则 `RegExp#exec` 返回的数组
if (length && typeof array[0] == 'string' && hasOwnProperty.call(array, 'index')) {
result.index = array.index
result.input = array.input
}
return result
}
// ... 未完待续,最后部分有数组遍历赋值
传入的对象是数组时,构造一个相同长度的数组 new array.constructor(length)
,这里相当于 new Array(length)
,因为 array.constructor === Array
。
var a = [];
a.constructor === Array; // true
var a = new Array;
a.constructor === Array // true
如果存在正则 RegExp#exec
返回的数组,拷贝属性 index
和 input
。判断逻辑是 1、数组长度大于 0,2、数组第一个元素是字符串类型,3、数组存在 index
属性。
if (length && typeof array[0] == 'string' && hasOwnProperty.call(array, 'index')) {
result.index = array.index
result.input = array.input
}
其中正则表达式 regexObj.exec(str)
匹配成功时,返回一个数组,并更新正则表达式对象的属性。返回的数组将完全匹配成功的文本作为第一项,将正则括号里匹配成功的作为数组填充到后面。匹配失败时返回 null
。
var re = /quick\s(brown).+?(jumps)/ig;
var result = re.exec('The Quick Brown Fox Jumps Over The Lazy Dog');
console.log(result);
// [
// 0: "Quick Brown Fox Jumps" // 匹配的全部字符串
// 1: "Brown" // 括号中的分组捕获
// 2: "Jumps"
// groups: undefined
// index: 4 // 匹配到的字符位于原始字符串的基于0的索引值
// input: "The Quick Brown Fox Jumps Over The Lazy Dog" // 原始字符串
// length: 3
// ]
如果不是深拷贝,传入value
和 result
,直接返回浅拷贝后的数组。这里的浅拷贝方式就是循环然后复制。
if (!isDeep) {
return copyArray(value, result)
}
// 浅拷贝数组
function copyArray(source, array) {
let index = -1
const length = source.length
array || (array = new Array(length))
while (++index < length) {
array[index] = source[index]
}
return array
}
对象&函数
// 主线代码
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
}
其中 Object
的构造函数是一个函数对象。
var obj = new Object();
typeof obj.constructor;
// 'function'
var obj2 = {};
typeof obj2.constructor;
// 'function'
对于非常规类型对象,通过各自类型分别进行初始化。
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)
}
}
拷贝正则类型
// \w 用于匹配字母,数字或下划线字符,相当于[A-Za-z0-9_]
const reFlags = /\w*$/
function cloneRegExp(regexp) {
// 返回当前匹配的文本
const result = new regexp.constructor(regexp.source, reFlags.exec(regexp))
// 下一次匹配的起始索引
result.lastIndex = regexp.lastIndex
return result
}
初始化 Symbol
类型
const symbolValueOf = Symbol.prototype.valueOf
function cloneSymbol(symbol) {
return Object(symbolValueOf.call(symbol))
}
循环引用
构造了一个栈用来解决循环引用的问题。
// 主线代码
stack || (stack = new Stack)
const stacked = stack.get(value)
// 已存在
if (stacked) {
return stacked
}
stack.set(value, result)
如果当前需要拷贝的值已存在于栈中,说明有环,直接返回即可。栈中没有该值时保存到栈中,传入 value
和 result
。这里的 result
是一个对象引用,后续对 result
的修改也会反应到栈中。
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
。
Symbol & 原型链
这里我们介绍下 Symbol
和 原型链属性的拷贝,通过标志位 isFull
和 isFlat
来控制是否拷贝。
// 主线代码
// 类型化数组对象
if (isTypedArray(value)) {
return result
}
const keysFunc = isFull // 拷贝 Symbol 标志位
? (isFlat // 拷贝原型链属性标志位
? getAllKeysIn // 包含自身和原型链上可枚举属性名以及 Symbol
: getAllKeys) // 仅包含自身可枚举属性名以及 Symbol
: (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
我们先来看下怎么获取自身、原型链、Symbol 这几种属性名组成的数组 keys
。
// 创建一个包含自身和原型链上可枚举属性名以及 Symbol 的数组
// 使用 for...in 遍历
function getAllKeysIn(object) {
const result = keysIn(object)
if (!Array.isArray(object)) {
result.push(...getSymbolsIn(object))
}
return result
}
// 创建一个仅包含自身可枚举属性名以及 Symbol 的数组
// 非 ArrayLike 数组使用 Object.keys
function getAllKeys(object) {
const result = keys(object)
if (!Array.isArray(object)) {
result.push(...getSymbols(object))
}
return result
}
上面通过 keysIn
和 keys
获取常规可枚举属性,通过 getSymbolsIn
和 getSymbols
获取 Symbol
可枚举属性。
// 创建一个包含自身和原型链上可枚举属性名的数组
// 使用 for...in 遍历
function keysIn(object) {
const result = []
for (const key in object) {
result.push(key)
}
return result
}
// 创建一个仅包含自身可枚举属性名的数组
// 非 ArrayLike 数组使用 Object.keys
function keys(object) {
return isArrayLike(object)
? arrayLikeKeys(object)
: Object.keys(Object(object))
}
// 测试代码
function Foo() {
this.a = 1
this.b = 2
}
Foo.prototype.c = 3
keysIn(new Foo)
// ['a', 'b', 'c'] (迭代顺序无法保证)
keys(new Foo)
// ['a', 'b'] (迭代顺序无法保证)
常规属性遍历原型链用的是 for.. in
,那么 Symbol
是如何遍历原型链的呢,这里通过循环以及使用 Object.getPrototypeOf
获取原型链上的 Symbol
。
// 创建一个包含自身和原型链上可枚举 Symbol 的数组
// 通过循环和使用 Object.getPrototypeOf 获取原型链上的 Symbol
function getSymbolsIn (object) {
const result = []
while (object) { // 循环
result.push(...getSymbols(object))
object = Object.getPrototypeOf(Object(object))
}
return result
}
// 创建一个仅包含自身可枚举 Symbol 的数组
// 通过 Object.getOwnPropertySymbols 获取 Symbol 属性
const nativeGetSymbols = Object.getOwnPropertySymbols
const propertyIsEnumerable = Object.prototype.propertyIsEnumerable
function getSymbols (object) {
if (object == null) { // 判空
return []
}
object = Object(object)
return nativeGetSymbols(object)
.filter((symbol) => propertyIsEnumerable.call(object, symbol))
}
我们回到主线代码,获取到 keys
组成的 props
数组之后,遍历并递归。
// 主线代码
const props = isArr ? undefined : keysFunc(value)
arrayEach(props || value, (subValue, key) => {
// props 时替换 key 和 subValue,因为 props 里面的 subValue 只是 value 的 key
if (props) {
key = subValue
subValue = value[key]
}
// 递归拷贝(易受调用堆栈限制)
assignValue(result, key, baseClone(subValue, bitmask, customizer, key, value, stack))
})
// 返回结果,主线结束
return result
我们看下 arrayEach
的实现,主要实现了一个遍历,并在 iteratee
返回为 false 时退出。
// 迭代数组
// iteratee 是每次迭代调用的函数
function arrayEach(array, iteratee) {
let index = -1
const length = array.length
while (++index < length) {
if (iteratee(array[index], index, array) === false) {
break
}
}
return array
}
我们看下 assignValue
的实现,在值不相等情况下,将 value 分配给 object[key]
。
const hasOwnProperty = Object.prototype.hasOwnProperty
// 如果现有值不相等,则将 value 分配给 object[key]。
function assignValue(object, key, value) {
const objValue = object[key]
// 不相等
if (! (hasOwnProperty.call(object, key) && eq(objValue, value)) ) {
// 值可用
if (value !== 0 || (1 / value) == (1 / objValue)) {
baseAssignValue(object, key, value)
}
// 值未定义而且键 key 不在对象中
} else if (value === undefined && !(key in object)) {
baseAssignValue(object, key, value)
}
}
// 赋值基本实现,其中没有值检查。
function baseAssignValue(object, key, value) {
if (key == '__proto__') {
Object.defineProperty(object, key, {
'configurable': true,
'enumerable': true,
'value': value,
'writable': true
})
} else {
object[key] = value
}
}
// 比较两个值是否相等
// (value !== value && other !== other) 是为了判断 NaN
function eq(value, other) {
return value === other || (value !== value && other !== other)
}
baseClone完整代码
这部分就是核心代码了,各功能分割如下,详细功能实现部分将对各个功能详细解读。
/**
* The base implementation of `_.clone` and `_.cloneDeep` which tracks
* traversed objects.
*
* @private
* @param {*} value The value to clone.
* @param {boolean} bitmask The bitmask flags.
* 1 - Deep clone
* 2 - Flatten inherited properties
* 4 - Clone symbols
* @param {Function} [customizer] The function to customize cloning.
* @param {string} [key] The key of `value`.
* @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
// 标志位
const isDeep = bitmask & CLONE_DEEP_FLAG // 深拷贝,true
const isFlat = bitmask & CLONE_FLAT_FLAG // 拷贝原型链,false
const isFull = bitmask & CLONE_SYMBOLS_FLAG // 拷贝 Symbol,true
// 为 cloneWith 方法提供自定义 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
}
比较
方法 | 性能 | 兼容性 | 支持复杂结构(循环、函数、Symbol 等) | 适用场景 |
---|---|---|---|---|
JSON.parse(JSON.stringify(obj)) | 🚀 快 | ✅ 高 | ❌ 不支持循环、函数、undefined 、Date 、Map 等 | 简单对象 |
lodash.cloneDeep(obj) | 🐢 慢 | ✅ 高 | ✅ 支持大部分情况,支持循环引用 | 大多数项目通用 |
structuredClone(obj) | 🚀🚀 非常快 | ❌ 新浏览器才支持 | ✅ 内建支持 Date 、Map 、Set 、循环引用等 | 现代浏览器或 Node 17+ |
手写递归(深拷贝) | 🐢~🚀 性能因实现而异 | ✅ 自定义 | ❌ 默认不支持复杂结构,需手动处理 | 学习用途或高度定制 |