深拷贝与浅拷贝
1.浅拷贝
浅拷贝是指,一个新的对象对原始对象的属性值进行精确地拷贝,如果拷贝的是基本数据类型,拷贝的就是基本数据类型的值,如果是引用数据类型,拷贝的就是内存地址。如果其中一个对象的引用内存地址发生改变,另一个对象也会发生变化。
(1)直接赋值
let arr1 = [1, 2, 3]
let arr2 = arr1
new2[0] = 0
console.log(arr1) // [0, 2, 3]
console.log(arr1) // [0, 2, 3]
console.log(arr1 === arr2) // true
(2)Object.assign()
object.assign 是 ES6 中 object 的一个方法,该方法可以用于 JS 对象的合并等多个用途,其中一个用途就是可以进行浅拷贝。该方法接受的第一个参数是目标对象,其余参数是源对象,用法:Object.assign(target, source_1, ···),该方法可以实现浅拷贝,也可以实现一维对象的深拷贝。
注意:
- 如果目标对象和源对象有同名属性,或者多个源对象有同名属性,则后面的属性会覆盖前面的属性。
- 如果该函数只有一个参数,当参数为对象时,直接返回该对象;当参数不是对象时,会先将参数转为对象然后返回。
- 因为 null 和 undefined 不能转化为对象,所以第一个参数不能为 null 或 undefined,会报错。
- 它不会拷贝对象的继承属性,不会拷贝对象的不可枚举的属性,可以拷贝 Symbol 类型的属性。
let target = { a: 1 }
let object2 = { b: 2 }
let object3 = { c: 3 }
Object.assign(target, object2, object3)
console.log(target) // {a: 1, b: 2, c: 3}
(3)扩展运算符
使用扩展运算符可以在构造字面量对象的时候,进行属性的拷贝。语法:let cloneObj = { ...obj };
let obj1 = { a: 1, b: { c: 1 } }
let obj2 = { ...obj1 }
obj1.a = 2
console.log(obj1) //{a:2,b:{c:1}}
console.log(obj2) //{a:1,b:{c:1}}
obj1.b.c = 2
console.log(obj1) //{a:2,b:{c:2}}
console.log(obj2) //{a:1,b:{c:2}}
扩展运算符 和 object.assign 有同样的缺陷,也就是实现的浅拷贝的功能差不多,但是如果属性都是基本类型的值,使用扩展运算符进行浅拷贝会更加方便。
(4)数组方法实现数组浅拷贝
(1)Array.prototype.slice
slice()方法是 JavaScript 数组的一个方法,这个方法可以从已有数组中返回选定的元素:用法:array.slice(start, end),该方法不会改变原始数组。该方法有两个参数,两个参数都可选,如果两个参数都不写,就可以实现一个数组的浅拷贝。
let arr = [1, 2, 3, 4]
console.log(arr.slice()) // [1,2,3,4]
console.log(arr.slice() === arr) //false
(2)Array.prototype.concat
concat() 方法用于合并两个或多个数组。此方法不会更改现有数组,而是返回一个新数组。该方法有两个参数,两个参数都可选,如果两个参数都不写,就可以实现一个数组的浅拷贝。
let arr = [1, 2, 3, 4]
console.log(arr.concat()) // [1,2,3,4]
console.log(arr.concat() === arr) //false
(5)手写实现浅拷贝
根据以上对浅拷贝的理解,实现一个浅拷贝的大致思路分为两点:
- 对基础类型做一个最基本的一个拷贝;
- 对引用类型开辟一个新的存储,并且拷贝一层对象属性。
// 浅拷贝的实现;
function shallowCopy(object) {
// 只拷贝对象
if (!object || typeof object !== 'object') return
// 根据 object 的类型判断是新建一个数组还是对象
let newObject = Array.isArray(object) ? [] : {}
// 遍历 object,并且判断是 object 的属性才拷贝
for (let key in object) {
if (object.hasOwnProperty(key)) {
newObject[key] = object[key]
}
}
return newObject
}
2. 深拷贝
深拷贝是指,对于简单数据类型直接拷贝他的值,对于引用数据类型,在堆内存中开辟一块内存用于存放复制的对象,并把原有的对象类型数据拷贝过来,这两个对象相互独立,属于两个不同的内存地址,修改其中一个,另一个不会发生改变。
(1)Object.assign()
Object.assign() 方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。
let obj1 = { person: { name: 'kobe', age: 41 }, sports: 'basketball' }
let obj2 = Object.assign({}, obj1)
obj2.person.name = 'wade'
obj2.sports = 'football'
console.log(obj1) // { person: { name: 'wade', age: 41 }, sports: 'basketball' }
(2)JSON.stringify()
JSON.parse(JSON.stringify(obj))是目前比较常用的深拷贝方法之一,它的原理就是利用 JSON.stringify 将 js 对象序列化(JSON 字符串),再使用 JSON.parse 来反序列化(还原)js 对象。
这个方法可以简单粗暴的实现深拷贝,但是还存在问题,拷贝的对象中如果有函数,undefined,symbol,当使用过 JSON.stringify()进行处理之后,都会消失。
let obj1 = {
a: 0,
b: {
c: 0,
},
}
let obj2 = JSON.parse(JSON.stringify(obj1))
obj1.a = 1
obj1.b.c = 1
console.log(obj1) // {a: 1, b: {c: 1}}
console.log(obj2) // {a: 0, b: {c: 0}}
使用该方法时,需要注意以下几点:
- 无法拷贝不可枚举的属性;
- 无法拷贝对象的原型链;
- 拷贝 RegExp 引用类型会变成空对象;
- 对象中含有 NaN、Infinity 以及 -Infinity,JSON 序列化的结果会变成 null;
无法拷贝对象的循环应用,即对象成环 (obj[key] = obj)。
(3)函数库 lodash 的_.cloneDeep 方法
该函数库也有提供_.cloneDeep 用来做 Deep Copy
var _ = require('lodash')
var obj1 = {
a: 1,
b: { f: { g: 1 } },
c: [1, 2, 3],
}
var obj2 = _.cloneDeep(obj1)
console.log(obj1.b.f === obj2.b.f) // false
(4)手写实现深拷贝函数
function clone(source) {
//判断source是不是对象
if (source instanceof Object == false) return source
//判断source是对象还是数组
let target = Array.isArray(source) ? [] : {}
for (let i in source) {
if (source.hasOwnProperty(i)) {
//判断数据i的类型
if (typeof source[i] === 'object') {
target[i] = clone(source[i])
} else {
target[i] = source[i]
}
}
}
return target
}
console.log(clone({ b: { c: { d: 1 } } })) // {b: {c: {d: 1}}})
虽然利用递归能实现一个深拷贝,但是同上面的 JSON.stringfy 一样,还是有一些问题没有完全解决,例如:
- 这个深拷贝函数并不能复制不可枚举的属性以及 Symbol 类型;
- 这种方法只是针对普通的引用类型的值做递归复制,而对于 Array、Date、RegExp、Error、Function 这样的引用类型并不能正确地拷贝;
- 对象的属性里面成环,即循环引用没有解决。
3. 解决递归爆栈
我们使用递归的方法对数据进行拷贝,但是这也会出现一个问题,递归的深度的深度太深就会引发栈内存的溢出,我们使用下面的方法来解决递归爆栈的问题:将待拷贝的对象放入栈中,循环直至栈为空。
function cloneLoop(x) {
const root = {}
// 栈
const loopList = [
{
parent: root,
key: undefined,
data: x,
},
]
while (loopList.length) {
// 深度优先
const node = loopList.pop()
const parent = node.parent
const key = node.key
const data = node.data
// 初始化赋值目标,key为undefined则拷贝到父元素,否则拷贝到子元素
let res = parent
if (typeof key !== 'undefined') {
res = parent[key] = {}
}
for (let k in data) {
if (data.hasOwnProperty(k)) {
if (typeof data[k] === 'object') {
// 下一次循环
loopList.push({
parent: res,
key: k,
data: data[k],
})
} else {
res[k] = data[k]
}
}
}
}
return root
}
这样我们就解决了递归爆栈的问题,但是循环引用的问题依然存在。
4. 解决循环引用
举例:当 a 对象的中的某属性值为 a 对象,这样就会造成循环引用。 我们使用暴力破解的方法来解决循环引用的问题。 思路:引入一个数组 uniqueList 用来存储已经拷贝的数组,每次循环遍历时,先判断对象是否在 uniqueList 中了,如果在的话就不执行拷贝逻辑了
function cloneForce(x) {
const uniqueList = [] // 用来去重
let root = {}
const loopList = [
{
parent: root,
key: undefined,
data: x,
},
]
while (loopList.length) {
const node = loopList.pop()
const parent = node.parent
const key = node.key
const data = node.data
// 初始化赋值目标,key为undefined则拷贝到父元素,否则拷贝到子元素
let res = parent
if (typeof key !== 'undefined') {
res = parent[key] = {}
}
// 数据已经存在
let uniqueData = find(uniqueList, data)
if (uniqueData) {
parent[key] = uniqueData.target
continue
}
// 数据不存在
// 保存源数据,在拷贝数据中对应的引用
uniqueList.push({
source: data,
target: res,
})
for (let k in data) {
if (data.hasOwnProperty(k)) {
if (typeof data[k] === 'object') {
loopList.push({
parent: res,
key: k,
data: data[k],
})
} else {
res[k] = data[k]
}
}
}
}
return root
}
//find函数用来遍历uniqueList
function find(arr, item) {
for (let i = 0; i < arr.length; i++) {
if (arr[i].source === item) {
return arr[i]
}
}
return null
}
5. 总结
- 浅拷贝:浅拷贝指的是将一个对象的属性值复制到另一个对象,如果有的属性的值为引用类型的话,那么会将这个引用的地址复制给对象,因此两个对象会有同一个引用类型的引用。浅拷贝可以使用 Object.assign 和展开运算符来实现。
- 深拷贝:深拷贝相对浅拷贝而言,如果遇到属性值为引用类型的时候,它新建一个引用类型并将对应的值复制给它,因此对象获得的一个新的引用类型而不是一个原有类型的引用。深拷贝对于一些对象可以使用 JSON 的两个函数来实现,但是由于 JSON 的对象格式比 js 的对象格式更加严格,所以如果属性值里边出现函数或者 Symbol 类型的值时,会转换失败
// 浅拷贝的实现;
function shallowCopy(object) {
// 只拷贝对象
if (!object || typeof object !== 'object') return
// 根据 object 的类型判断是新建一个数组还是对象
let newObject = Array.isArray(object) ? [] : {}
// 遍历 object,并且判断是 object 的属性才拷贝
for (let key in object) {
if (object.hasOwnProperty(key)) {
newObject[key] = object[key]
}
}
return newObject
}
// 深拷贝的实现;
function deepCopy(object) {
if (!object || typeof object !== 'object') return
let newObject = Array.isArray(object) ? [] : {}
for (let key in object) {
if (object.hasOwnProperty(key)) {
newObject[key] =
typeof object[key] === 'object'
? deepCopy(object[key])
: object[key]
}
}
return newObject
}