js深浅拷贝原理以及方法

  • js深浅拷贝相关

说到js深浅拷贝的问题就需要说到到js的数据类型,js的数据类型主要分为 基本数据类型 和 引用数据类型:

  1. 基本数据类型:Number\String\Boolean\null\undefined\Symbol\bigInt
  2. 引用类型:Object(Array\Function)

在JavaScript中,每一个变量在内存中都需要一个空间来存储,其中的基本数据类型存放在栈内存,复杂数据类型(也就是引用类型)则存放在堆内存中,堆内存用于存放由 new 创建的对象,栈内存存放一些基本类型的变量和对象的引用变量

由于引用类型的值实际存储在堆内存中,它在栈中只存储了一个固定长度的地址,这个地址指向堆内存中的值,这样拷贝的时候,就出现两种情况:拷贝地址 和 拷贝值,也就是JS 中的常见的浅深拷贝问题,

浅拷贝:

创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。

深拷贝:

将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象

数组深拷贝实现:

一、slice 方法

将原数组中抽离部分出来形成一个新数组。我们只要设置为抽离全部,即可完成数组的深拷贝

1
2
3
4
5
6
7
8
9
10
11
var arr = [1,2,3,4,5]
var arr2 = arr.slice(0)
arr[2] = 5
console.log(arr) // 输出 [1, 2, 5, 4, 5]
console.log(arr2) // 输出 [1, 2, 3, 4, 5]

// 拷贝成功,但是如果数组里面嵌套对象,这种方式就不能完成嵌套对象的深拷贝了
var b = [{name: 'zhangsan', age: 18}]
var b1 = b.slice(0)
b1[0].name = 'lisi'
console.log(b) // 输出 [{name: 'lisi', age: 18}]

二、concat 方法

它是用于连接多个数组组成一个新的数组的方法。那么,我们只要连接它自己,即可完成数组的深拷贝

1
2
3
4
5
var arr = [1,2,3,4,5]
var arr2 = arr.concat()
arr[2] = 5
console.log(arr) // 输出 [1, 2, 5, 4, 5]
console.log(arr2) // 输出 [1, 2, 3, 4, 5]

三、扩展运算符(…)

1
2
3
4
5
var arr = [1,2,3,4,5]
var [ ...arr2 ] = arr
arr[2] = 5
console.log(arr)
console.log(arr2)

concat 以及 扩展运算符这两种方法同样,如果数组里面嵌套对象,就不能完成嵌套对象的深拷贝了,可以利用递归遍历赋值的形式完成深层复杂数据深拷贝,介绍对象深拷贝的时候会讲到

对象深拷贝实现:

一、最简单版本

1
JSON.parse(JSON.stringify());

这种写法非常简单,但是它还是有以下缺陷:

  1. 被拷贝的对象中某个属性的值为undefined,拷贝之后该属性会丢失

    1
    2
    3
    4
    5
    6
    7
    8
    9
    var a = { name: 'zhangsan', age: undefined }
    var a1 = JSON.parse(JSON.stringify(a))
    console.log(a1) // { name: 'zhangsan' }
    // 能看到数值为undefined的属性,拷贝后 直接 丢失了

    // 如果数组中的对象值为undefined,拷贝也同样发生丢失问题
    var b = [{ name: undefined }]
    var b1 = JSON.parse(JSON.stringify(b))
    console.log(b1) // 输出:[{}]
  2. 如果拷贝的对象属性值有function的话,拷贝之后该属性会丢失

    1
    2
    3
    4
    var a = { name: function () {}}
    var a1 = JSON.parse(JSON.stringify(a))
    console.log(a1) // 输出:{}
    // 可以输出看到没有拷贝到name属性
  3. 如果被拷贝的对象中有正则表达式,则拷贝之后的对象正则表达式会变成Object

    1
    2
    3
    var a = {name: /bbb/}
    var a1 = JSON.parse(JSON.stringify(a))
    console.log(a1) // 输出:{name: {}}

二、Object.assign

Object.assign方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)

1
2
3
4
5
6
7
8
9
10
11
var a = { name: 'nihao',  age: 18 }
var a1 = Object.assign(a)
a1.age = 26
console.log(a.age) // 输出 18
// 通过输出可以看到深拷贝成功,修改a1的age 并没有影响a的age值
// 但是当对象里面还有对象嵌套的时候,就没法完成深拷贝了
var b = { name: 'nihao', age: 18, friend: { name: 'lisi', age: 18 } }
var b1 = Object.assign(b)
b1.friend.age = 26
console.log(b.friend.age) // 输出 26
// 可以看到Object.assign只支持一层的深拷贝

三、扩展运算符(…)

1
2
3
4
5
6
7
8
9
let obj = {
a: {
a1: 'a1'
},
b: 'b'
}
let ass = {...obj}
ass.a.a1 = 'aaa'
ass.b = 'bbb'

扩展运算符(…) 和 Object.assign 一样也只能进行一层的深拷贝,里面嵌套的对象只能浅拷贝

四、递归遍历赋值

如果对象里面的属性没有复杂数据类型的时候,直接遍历赋值就可以了

1
2
3
4
5
6
7
function clone () {
let cloneItem = {}
for (let k in target) {
cloneItem[k] = target[k]
}
return cloneItem
}

但是考虑到我们要拷贝的对象是不知道有多少层深度的,所以我们可以想到可以利用递归来解决:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function clone (target) {
if (typeof target === 'object') {
let cloneItem = Array.isArray(target) ? [] : {} // 考虑到数组或者对象
for (let key in target) {
cloneItem[key] = clone(target[key])
}
return cloneItem
} else {
return target
}
}
// 通过一个小栗子来测试一下:
var a = {
name: 'nihao',
age: undefined,
sayHello: function () {},
friends: [{
name: 'zhangsan',
age: 18
},{
name: 'lisi',
age: undefined
}]
}
console.log(clone(a)) // 通过输出可以看到,完美拷贝成功

但是这样的递归赋值,也有它的缺点:不支持对象属性为正则的拷贝,容易造成引用死循环。

1
2
3
4
5
6
7
var a = {
name: 'nihao',
age: undefined,
sayHello: function () {},
target: a
}
console.log(clone(a)) // 因为对象的属性间接或直接的引用了自身,这样递归遍历的时候就会发生死循环,造成栈内存溢出

Comentarios

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×