对象引用和复制

JavaScript 中对象(Object)与其他简单数据类型的不同点在于对象在内存中是通过“引用”拿到存储的数据的,而其他简单数据类型,例如string和number在内存中的数据是“一体的”的。

例如下面有两个数据

js
1let a = 1
2let b = { name: 'qyx' }

在内存中,它们的存储方式类似这样:

image-20230207210720265

所谓的引用,实际上是该对象在内存中的地址,也就是说赋值的b 变量保存着数据在内存中的地址,JavaScript 通过这个地址来获取内存中的实际数据。

这样做的结果是拷贝对象和拷贝简单数据类型的逻辑是不一样的。

当我们拷贝简单数据类型时:

js
1let a = 1
2let cloneA = a

内存中就会出现两个数据。

image-20230207210744794

当我们用同样的方法拷贝复杂数据类型时:

js
1let b = { name: 'qyx' }
2let cloneB = b

仅仅拷贝了 b 对象中保存的地址,所以最终内存中有两个变量名引用了同一个数据

image-20230207211041394

当我们用其中一个变量修改数据时,另一个变量能访问到被修改后的数据。

js
1let b = { name: 'qyx' }
2let cloneB = b
3b.name = 'qiuyanxi'
4cloneB.name // "qiuyanxi"

对象之间的比较

只有对象保存的地址是一样的,对象之间的相等比较才会相等。

js
1let a = { name: 'qiuyanxi' }
2let b = a
3a === b // true

如果保存地址不一样,即使看起来两个数据是一样的,相等比较也会返回 false

js
1let a = { name: 'qiuyanxi' }
2let b = { name: 'qiuyanxi' }
3a === b // false

对象浅拷贝的方式

通常我们会使用let clone= {...user}语法来浅拷贝对象。

还有一个很简单的 API,能够“浅”拷贝对象,不过通常我们用它来合并多个对象的属性——Object.assign

js
1Object.assign(dest, [src1, src2, src3...])

第一个属性 dest 是目标对象,后面的 src 则是多个对象

js
1let dest = {}
2Object.assign(dest, { name: 'qyx' }, { age: 30 })
3dest // {name: 'qyx', age: 30}

如果属性名已经存在,那么会默认覆盖:

js
1let user = { name: 'John' }
2
3Object.assign(user, { name: 'Pete' })
4
5alert(user.name) // 现在 user = { name: "Pete" }

除此之外,还有很多方法可以做到。

对象深拷贝的方式

对象中的属性还可以保存对象——实际上是其他对象的引用。

js
1let user = {
2  name: 'John',
3  sizes: {
4    height: 182,
5    width: 50,
6  },
7}
8
9alert(user.sizes.height) // 182

现在这样拷贝 clone.sizes = user.sizes 已经不足够了,因为 user.sizes 是个对象,它会以引用形式被拷贝。因此 clone 和 user 会共用一个 sizes:

js
1let user = {
2  name: 'John',
3  sizes: {
4    height: 182,
5    width: 50,
6  },
7}
8
9let clone = Object.assign({}, user)
10
11alert(user.sizes === clone.sizes) // true,同一个对象
12
13// user 和 clone 分享同一个 sizes
14user.sizes.width++ // 通过其中一个改变属性值
15alert(clone.sizes.width) // 51,能从另外一个获取到变更后的结果

为了解决这个问题,我们需要用到深拷贝。我们应该使用一个拷贝循环来检查 user[key] 的每个值,如果它是一个对象,那么也复制它的结构。这就是所谓的“深拷贝”。

最终我们会让 user 和 clone 成为两个真正独立的对象。

  • 最简单的深拷贝

    JSON.stringify和JSON.parse,不过这种方法局限性很多,比如不能拷贝函数。

    js
    1const obj = {
    2  name: 'yanxi',
    3  props: { name: 'qiu' },
    4}
    5const obj2 = JSON.parse(JSON.stringify(obj))
    6obj2.props.name = '11111'
    7obj
    8//{name: "yanxi", props: {name: "qiu"}}
  • 最方便的深拷贝

    lodash 库的 _.cloneDeep(obj)。

  • 自己实现的深拷贝

    自己动手实现一个深拷贝

总结

对象通过引用被赋值和拷贝。

一个变量存储的不是“对象的值”,而是一个对值的“引用”(内存地址)。因此,拷贝此类变量或将其作为函数参数传递时,所拷贝的是引用,而不是对象的数据本身。

所有通过被拷贝的引用的操作(如添加、删除属性)都作用在同一个对象上。

为了创建“真正的拷贝”(一个克隆),我们可以使用 Object.assign 来做所谓的“浅拷贝”(嵌套对象被通过引用进行拷贝)或者使用“深拷贝”函数,例如 _.cloneDeep(obj)。