实现浅拷贝、深拷贝
在程序开发中,我们经常会遇到需要复制一个对象的场景。对象复制有两种方式:浅拷贝和深拷贝,这也是面试中经常会碰到的经典问题。
前置知识
数据分类
如下图所示, JS
中数据类型大体可划分为, 基本数据
和 引用数据
两大类
基本数据
存储方式
在 JS
中 基本数据
是存储在 栈内存(Stack Memory)
中, 它们的值是直接存储在变量访问的位置
那么什么是 栈内存
呢? 它是一种计算机内存中划分出来的一块 连续
的 存储区域
, 它的主要特点是 先进后出
当我们创建一个 基本数据
的变量时, 因为它占用空间小、大小固定, 所以会在 栈内存
中分配一个固定大小的空间来存储这个值, 当这个变量不再被使用时, 它所占用的空间会被自动释放, 因此 基本数据
的赋值和拷贝操作非常快速和高效
引用数据
存储方式
在 JS
中 引用数据
是存储在 堆内存(Heap Memory)
中的, 因为它们的大小是不确定的, 对象的属性和方法可能会动态增加或删除
那么什么是 堆内存
呢? 它是一种计算机内存中划分出来的一块 非连续
的 存储区域
, 它的特点是可以动态分配和释放内存空间, 但它需要手动管理内存空间
当我们创建一个 引用数据
, 会在 堆内存
中分配一个内存空间用来存储对象的所有属性和方法, 然后在 栈内存
中创建一个指向该内存空间的 指针
, 这个指针存储在变量访问的位置, 当这个变量不再被使用时,栈内存
中的指针被销毁, 但 堆内存
中的对象空间不会自动释放, 需要手动调用 垃圾回收机制
来释放这些空间
浅拷贝
手写实现
通过手写一个浅拷贝方法, 来更深入了解
浅拷贝
, 总体思路如下:
- 如果拷贝对象是个
基本数据
, 则直接返回该值- 新建一个对象
- 循环对象的所有属性, 并拷贝属性值, 如果该属性是
引用s数据
拷贝的则是数据的引用地址
const clone = (target) => {
// 1. 对于基本数据类型(string、number、boolean……), 直接返回
if (typeof target !== 'object' || target === null) {
return target
}
// 2. 创建新对象
const cloneTarget = Array.isArray(target) ? [] : {}
// 3. 循环 + 递归处理
Object.keys(target).forEach(key => {
cloneTarget[key] = target[key];
})
return cloneTarget
}
const res= clone({ name: 1, user: { age: 18 } })
Object.assign()
Object.assign(target, ...sources)
方法将 sources
中所有的源对象的可枚举属性复制到目标对象 target
中, 最后返回修改后的 target
对象
const address = ['杭州']
const base1 = {
age: 18,
name: 'lh',
}
const base2 = {
age: 20,
address,
name: 'moyuanjun',
}
// 将 base1 base2 中的属性添加到, 对象 {} 中
// base1 base2 存在相同属性, 会被 base2 的覆盖掉
const res = Object.assign({}, base1, base2)
res.address === address // true
res.address === base2.address // true
复制代码
关于
Object.assign()
更多细节参考 MDN
展开运算符 ...
展开运算符 ...
, 可以数组或对象在语法层面展开, 从而实现数组或对象的一个浅拷贝
const address = ['杭州']
const base1 = {
age: 18,
name: 'lh',
}
const base2 = {
age: 20,
address,
name: 'moyuanjun',
}
const res = { ...base1, ...base2 }
res.address === address // true
res.address === base2.address // true
复制代码
关于
展开运算符
更多细节参考 MDN
数组方法
对于数组可以使用, 数组的一些方法进行拷贝, 比如: Array.prototype.concat()
Array.prototype.slice()
Array.from
等方法, 它们的特点都是不改变原数组、同时返回一个新的数组
const base = {
age: 18,
name: 'lh',
}
const arr = [1, 'moyuanjun', base]
arr.concat([])
arr.slice()
Array.from(arr)
复制代码
第三方库
可以使用一些第三方库提供的工具方法来实现拷贝, 比如: lodash
中的 clone
方法
const base = {
name: '张三',
age: 18,
}
const obj = {
base,
address: ['杭州'],
}
const res = _.clone(obj)
深拷贝
手写实现
通过手写一个
深拷贝
方法, 来更深入了解深拷贝
, 总体思路如下:
- 目标类型判断, 如果是非
引用数据
则直接返回- 针对特殊的
引用数据
进行单独处理- 判断当前拷贝的目标数据, 是否已经拷贝过, 如果拷贝过则返回上次拷贝的数据: 目的是解决
共同引用
、循环引用
等问题- 新建一个对象
- 循环对象的所有属性, 如果属性值是个
基本数据
, 则直接返回该值, 如果属性值是个引用数据
, 则需要递归调用(新建对象、拷贝属性……)
// map 用于记录出现过的对象, 解决循环引用
const deepClone = (target, map = new WeakMap()) => {
// 1. 对于基本数据类型(string、number、boolean……), 直接返回
if (typeof target !== 'object' || target === null) {
return target
}
// 2. 函数 正则 日期 MAP Set: 执行对应构造题, 返回新的对象
const constructor = target.constructor
if (/^(Function|RegExp|Date|Map|Set)$/i.test(constructor.name)) {
return new constructor(target)
}
// 3. 解决 共同引用 循环引用等问题
// 借用 `WeakMap` 来记录每次复制过的对象, 在递归过程中, 如果遇到已经复制过的对象, 则直接使用上次拷贝的对象, 不重新拷贝
if (map.get(target)) {
return map.get(target)
}
// 4. 创建新对象
const cloneTarget = Array.isArray(target) ? [] : {}
map.set(target, cloneTarget)
// 5. 循环 + 递归处理
Object.keys(target).forEach(key => {
cloneTarget[key] = deepClone(obj[key], map);
})
// 6. 返回最终结果
return cloneTarget
}
JSON.parse(JSON.stringify())
这里利用 JSON.stringify
将对象转成 JSON
字符串, 再用 JSON.parse
把字符串解析成对象, 如此一来一去就能够实现 引用数据
的一个深拷贝
const obj = {
age: 18,
name: 'moyuanjun',
}
const res = JSON.parse(JSON.stringify(obj))
复制代码
注意该方法的 6
个局限性:
NaN
Infinity
-Infinity
会被序列化为null
Symbol
undefined
function
会被忽略(对应属性会丢失)Date
将得到的是一个字符串- 拷贝
RegExp
Error
对象,得到的是空对象{}
const obj = {
num1: NaN,
num2: Infinity,
num3: -Infinity,
symbol: Symbol('xxx'),
name: undefined,
add: function(){},
date: new Date(),
reg: /a/ig,
error: new Error('错误信息')
}
console.log(JSON.parse(JSON.stringify(obj)))
// 打印结果
// {
// num1: null,
// num2: null,
// num3: null,
// date: '2023-03-03T03:40:38.594Z',
// reg: {},
// error: {}
// }
复制代码
- 多个属性如果复用同一个
引用数据
A
时, 拷贝的结果和原数据结构不一致(会完整拷贝多个引用数据
A
), 如下代码所示: 对象obj
中base
和children
指向同一个对象, 但是JSON.parse(JSON.stringify())
复制出来的对象res
中base
和children
指向了不同的对象, 也就是说拷贝后的res
对象和原对象obj
数据结构不一致
const base = {
name: '张三',
age: 18,
}
const obj = {
base,
children: base
}
const res = JSON.parse(JSON.stringify(obj))
// 原对象, obj.base obj.children 指向同一个对象
obj.base.name = '李四'
console.log(obj.base === obj.children) // true
console.log(obj.children.name) // 李四
// 拷贝后, res.base res.children 指向了不同对象, 拷贝了两个(数据结构被改了)
res.base.name = '李四'
console.log(res.base === res.children) // false
console.log(res.children.name) // 张三
复制代码
下图是对象
obj
和拷贝后对象res
的内存结构图
- 循环引用对象中使用将会报错
使用 JSON.stringify()
序列化循环引用的对象, 将会抛出错误
const base = {
name: '张三',
age: 18,
}
base.base = base
// TypeError: Converting circular structure to JSON
const res = JSON.parse(JSON.stringify(base))
复制代码
更对细节可参考 MDN
使用 structuredClone
structuredClone
是一个新的 API
可用于对数据进行 深拷贝
, 同时还支持循环引用
const base = {
name: '张三',
age: 18,
}
const obj = { base }
obj.obj = obj
const res = structuredClone(obj)
复制代码
注意: 使用 structuredClone
进行拷贝, 如果有个属性值是个函数, 方法会抛出错误
// DOMException [DataCloneError]: () => {} could not be cloned.
const res = structuredClone({
add: () => {}
})
复制代码
有关
structuredClone
更多信息查看 MDN
使用第三方库
可以使用一些第三方库提供的工具方法来实现拷贝, 比如: lodash
中的 cloneDeep
方法
const base = {
name: '张三',
age: 18,
}
const obj = { base }
obj.obj = obj
const res = _.cloneDeep(obj)
参考文章:
- 感谢你赐予我前进的力量