搞懂浅拷贝!内存分析+两个复杂例子|MDN文档系列01

52 阅读4分钟

docs/Glossary

浅拷贝

🤔 提出一些问题

  • 浅拷贝对象和源对象之间怎样互相影响?
  • “浅拷贝”是指:考虑拷贝对象的深浅的意思吗?
  • “浅”是指第一层共享,第二层、第三层就不共享了吗?
  • “浅拷贝”与状态管理中的概念 不可变更新(immutable) 有怎样的关系?

1. 数组里嵌套对象 + 内存分析

  • 有一个初始数组:
const arr1 = ["面条", { list: {...} }];
// 栈内存:
arr1 → 0x0010

// 堆内存:
0x0010Array object(arr1)
          ├─ 0"面条"
          └─ 10x0020
0x0020 → { list: {...} }
  • 使用 Array.from(arrayLike) 进行浅拷贝
const arr2 = Array.from(arr1);
// 栈内存:
arr1 → 0x0010
arr2 → 0x0030

// 堆内存:
0x0010Array object(arr1)
          ├─ 0"面条"
          └─ 10x0020

0x0030Array object(arr2)
          ├─ 0"面条"
          └─ 10x0020 // 同一个引用❗️

0x0020 → { list: {...} }
  • 改值类型:
// 第一种: 改源对象
arr1[0] = "火鸡";

此时arr1 = ["火鸡", { list: {...} }];
此时arr2 = ["面条", { list: {...} }];

// 第二种: 改浅拷贝对象
arr2[0] = "火鸡";

此时arr1 = ["面条", { list: {...} }];
此时arr2 = ["火鸡", { list: {...} }];

可以看到,无论是改源对象,还是改浅拷贝对象,另一方都没有改变。

所以,值类型并没有被共享。

  • 改引用类型:
// 第一种: 改源对象
arr1[1].list = { "小红帽", "大灰狼" };

此时arr1 = ["面条", { "小红帽", "大灰狼" }];
此时arr2 = ["面条", { "小红帽", "大灰狼" }];(跟随改变了❗️)

// 第二种: 改浅拷贝对象
arr2[1].list = { egg: '3个' , money: '10块零三毛'};

此时arr1 = ["面条", { egg: '3个' , money: '10块零三毛'}];(跟随改变了❗️)
此时arr2 = ["面条", { egg: '3个' , money: '10块零三毛'}];

这证明了:arr1的索引1和arr2的索引1,指向了同一个对象

  • 再来一个例子,只要上面的例子你看懂了,这个例子你就会觉得轻而易举:
// 第一种: 改源对象
arr1[1].list.egg.count.number = 99;

此时arr1 = ["面条", { list: { egg: { count: { number: 99 }, unit: "个" }, flour: "面粉", water: "水" }}];
此时arr2 = ["面条", { list: { egg: { count: { number: 99 }, unit: "个" }, flour: "面粉", water: "水" }}];(发生改变了❗️)

// 第二种: 改拷贝对象
arr2[1].list.egg.unit = { id: 'asdfasd001', emoji: '🐶' };

此时arr1 = ["面条", { list: { egg: { count: { number: 99 }, unit: { id: 'asdfasd001', emoji: '🐶' } }, flour: "面粉", water: "水" }}];(发生改变了❗️)
此时arr2 = ["面条", { list: { egg: { count: { number: 99 }, unit: { id: 'asdfasd001', emoji: '🐶' } }, flour: "面粉", water: "水" }}];

解释

因为arr1的索引1和arr2的索引1指向的是同一个对象,所以只要是这个共享对象(0x0020)内部的属性,无论这个属性多深,它都会跟随改变。

这也验证了我们刚刚的结论:arr1的索引1和arr2的索引1,指向了同一个对象

总结

官方MDN中的定义:浅拷贝是属性与拷贝的源对象属性共享相同引用的副本。

  • 通俗版解释:

    • 浅拷贝对象的变量本身(arr2),会获得一个新的地址(0x0030

    • 对于浅拷贝对象内部首层的每一个属性,如果该属性的值是引用类型,就会引用源对象中对应属性的对应值,也就是共享了相同的引用0x0020 )。

    • 所以我们管浅拷贝对象,叫做一个“副本”。

  • 专业版解释:

    • 浅拷贝会创建一个新的顶层对象(新的堆地址 0x0030),因此拷贝对象变量本身是独立的。
    • 对于浅拷贝对象内部首层属性,如果该属性的值是引用类型(对象、数组、函数等),则复制的是引用的副本,即两个对象共享相同的底层引用地址( 0x0020)。
    • 所以“浅拷贝对象”被称为源对象的副本,指的是它的外层结构独立于源对象,而内部引用保持共享。

2. 对象里嵌套数组 + 内存分析

==这次可以尝试猜一猜输出结果哦==

  • 有一个初始对象:
const obj1 = {
    emoji: '🥹',
    userList: [
        { name: 'A', age: 7 },
		{ name: 'B', age: 10 },
        { name: 'C', age: 1 },
        { name: 'D', age: 32 }
    ]
};

// 栈内存:
obj1 = 0x0030;

// 堆内存:
0x0030 → {
    emoji: '🥹',
    userList: [
        { name: 'A', age: 7 },
	     { name: 'B', age: 10 },
        { name: 'C', age: 1 },
        { name: 'D', age: 32 }
    ]
};
  • 使用 Object.assign(target, ...source) 进行浅拷贝:
const obj2 = Object.assign({}, obj1);

// 栈内存:
obj1 -> 0x0030
obj2 → 0x0040

// 堆内存:
0x0030 → {
    emoji: '🥹',
    userList: 0x0050
}

0x0040 → {
    emoji: '🥹'userList: 0x0050 // 同一个引用❗️
}

0x0050 → [
    { name: 'A', age: 7 },
    { name: 'B', age: 10 },
    { name: 'C', age: 1 },
    { name: 'D', age: 32 }
]