JavaScript中实现浅拷贝与深拷贝最详解(多种版本实现,轻松解决面试题)
浅拷贝很简单,因为它只涉及浅层的元素,所以一个循环直接 “=” 号 赋值就能实现。
深拷贝的实现就有点难度了,也是面试题常考的,因为它涉及的元素的深度不一,只要要拷贝的对象或数组内的子元素、子子元素、子子子元素… 再是对象或数组的话,就要再进行拷贝处理。所以深拷贝的实现方法也是很多的,下面将一 一讲解各种方法存在的问题和优化及其实现代码。
1. 浅拷贝
只进行浅层拷贝,相当于拷贝出来的对象或数组的属性都是被拷对象用 “=” 号赋值的。所以基本类型拷贝没问题, 属性值为对象或数组时拷贝的只是引用,指向同一内存地址 。
// 1. 使用ES6语法
function shallowClone1(target) {
// 类型判断,target为对象或数组才有意义
// typeof target === 'object' 有三种可能,对象、数组、null
if (typeof target === 'object' && target !== null) {
if (Array.isArray(target)) {
// 如果target为数组
return [...target]
} else {
// 为对象
return { ...target }
}
} else {
// 传入的参数不符合条件
return target
}
}
// 2. 使用基础语法
function shallowClone2(target) {
// 类型判断,target为对象或数组才有意义
// typeof target === 'object' 有三种可能,对象、数组、null
if (typeof target === 'object' && target !== null) {
// 根据target是对象或数组,创建对应的容器
const result = Array.isArray(target) ? [] : {}
// 遍历target
for (let key in target) {
// 检测该属性是否为对象本身的属性(不能拷贝原型对象的属性)
if (target.hasOwnProperty(key)) {
// 往新容器赋值
result[key] = target[key]
}
}
// 返回结果
return result
} else {
// 传入的参数不符合条件
return target
}
}
2. 深拷贝
进行深层拷贝,拷贝出来的对象内的 所有(包含深层)属性值为对象或数组的属性 都指向新的内存地址
实现深拷贝:
- 实现1:大众乞丐版(js内置的JSON模块)
- 问题1:函数属性会丢失
- 问题2:循环引用会出错
- 实现2:面试基础版(递归)
- 解决问题1:函数属性还没丢失
- 实现3:面试加强版(递归结合Map解决循环引用问题)
- 解决问题2:循环引用正常
- 实现4:最终版(优化遍历性能)
- 数组:while | for | forEach() 优于 for-in | keys() & forEach()
- 对象:for-in 与 keys() & forEach() 差不多
// ----------实现1:大众乞丐版(js内置的JSON模块)START-----------
function (target) {
// 类型判断
if (typeof target === 'object' && target !== null) {
// 先通过 JS 格式的对象或数组创建 JSON 格式字符串
// 再通过 JSON 字符串创建 JS 格式的对象或数组
return JSON.parse(JSON.stringify(target))
} else {
return target
}
}
// 问题1:函数属性丢失(JSON.stringify(),遇到属性为函数的会主动丢弃; JSON.parse()遇函数会报错)
deepClone1({ a: 1, b: '2', c: (a, b) => a + b }) // Object { a: 1, b: "2" }
//问题2:循环引用会出错
const obj1 = {a: 1, b: ['x', 'y'], c: {z: 100}}
obj1.b.push(obj1.c)
obj1.c.j = obj1.b
deepClone(obj1) // 报错:Uncaught TypeError: Converting circular structure to JSON
// 但单向引用不会出错,只是克隆之后是新对象,不再是引用关系
const obj11 = {a: 1, b: ['x', 'y'], c: {z: 100}}
obj11.b.push(obj11.c)
const cobj11 = deepClone(obj11) // {a: 1, b: ['x', 'y', {z: 100}], c: {z: 100}}
cobj11.c.z = 999
console.log(cobj11) // {a: 1, b: ['x', 'y', {z: 100}], c: {z: 999}}
// ----------实现1:大众乞丐版(js内置的JSON模块)END-----------
// ----------实现2:面试基础版(递归)START-----------
function deepClone2(target) {
// 类型判断,如果不是对象或数组,则直接返回target
if (typeof target === 'object' && target !== null) {
// 根据类型创建一个容器
const result = Array.isArray(target) ? [] : {}
// 遍历对象
for (let key in target) {
// 检测该属性是否为对象本身的属性(不能拷贝原型对象的属性)
if (target.hasOwnProperty(key)) {
// 递归拷贝
result[key] = deepClone2(target[key])
}
}
// 遍历完毕,返回新的对象或数组
return result
} else {
return target
}
}
// 解决问题1:函数属性没丢失
deepClone2({ a: 1, b: '2', c: (a, b) => a + b }) // {a: 1, b: '2', c: ƒ}
// 循环引用问题没有解决。
// 原因:循环引用会使递归不断在循环引用的对象或数组中执行,造成死循环,最后栈溢出,浏览器报错
// 但单向引用不会出错,只是克隆之后是新对象,不再是引用关系
// ----------实现2:面试基础版(递归)END-----------
// ----------实现3:面试加强版(递归结合Map解决循环引用问题) START-----------
// 实现3实际就是在实现2的基础上结合了Map
// Map和对象类似,只不过Map的键和值可以是任意类型
// 用Map做容器可确保遍历时键的唯一性
function deepClone3(target, map = new Map()) {
// 类型判断
if (typeof target === 'object' && target !== null) {
// 进行克隆前,判断对象或数组之前是否克隆过,如果克隆直接返回储存的值,阻断死循环
if (map.get(target)) return map.get(target)
let cache = map.get(target)
// 根据类型创建一个容器
const result = Array.isArray(target) ? [] : {}
// 将新的结果存入到Map中,
map.set(target, result)
// 遍历对象
for (let key in target) {
// 检测该属性是否为对象本身的属性(不能拷贝原型对象的属性)
if (target.hasOwnProperty(key)) {
// 递归拷贝
result[key] = deepClone3(target[key], map)
}
}
return result
} else {
return target
}
}
// 解决问题1:函数属性没丢失
//解决问题2:循环引用正常。循环引用其实就是不断的套娃,大家可以简单写段代码感受一下,如:
/*
const obj = { a: [1, 2, 3], b: { x: 'x', y: 'y' } }
obj.a.push(obj.b)
obj.b.z = obj.a
console.log(obj)
*/
// ----------实现3:面试加强版(递归结合Map解决循环引用问题) END-----------
// ----------实现4:最终版(优化遍历性能) START-----------
// 主要针对 实现3面试加强版 的优化
function deepClone4(target, map = new Map()) {
// 类型判断
if (typeof target === 'object' && target !== null) {
// 进行克隆前,判断对象或数组之前是否克隆过,如果克隆直接返回储存的值
if (map.get(target)) return map.get(target)
let cache = map.get(target)
// 根据类型创建一个容器
let isArray = Array.isArray(target)
const result = isArray ? [] : {}
// 将新的结果存入到Map中,
map.set(target, result)
// 判断是对象还是数组,再进行遍历(优化点)
if (isArray) {
// 为数组,forEach() 遍历
target.forEach((item, index) => {
result[index] = deepClone4(item, map)
})
} else {
// 为对象,keys() + forEach() 遍历
Object.keys(target).forEach(key => {
result[key] = deepClone4(target[key], map)
})
}
return result
} else {
return target
}
}
// ----------实现4:面试加强版2(优化遍历性能) END-----------