D20 - 浓浓咖啡香的深拷贝、浅拷贝

前言

什麽是拷贝? 今天朋友想 copy 你的报告,最简单的就是影印一份给他,但是当你修改报告中的内容时,发现朋友拷贝的那份也跟着修改了,哪尼,难道我见证了量子纠缠?!

量子的世界请去找老高,但在 JavaScript 中,这个称为 浅拷贝 的现象。

什麽是深拷贝、浅拷贝?

深拷贝与浅拷贝的概念像以下图示:

浅拷贝:复制过後部分 A、B 的值还是会互相影响
深拷贝:复制过後 A、B 是完全的独立个体,彼此值不影响

为什麽会有这两种拷贝差异呢?

背後原理要先带到 call by valuecall by sharing 观念。

Call By Value & Call By Sharing

关於这部分有很多大神的文章可以拜读,这里简单统整一下结论,JavaScript 中变数分为两种型别:

  • 基本型别:复制变数时,记忆体中会新增一个拥有同样值的新记忆体位置,呼叫变数会丢出在记忆体中存取的值,称为 call by value。

  • 物件型别:复制物件型别的变数时,实际上是复制记忆体中的地址,相同的记忆体位置指向一样的值; 对物件赋予新的值,则记忆体存的值也会跟着变动为新位置的地址,称为 call by sharing。

原始型别没有深浅拷贝之分,主要是物件型别的变数因记忆体存址指向的关系,浅拷贝指向的记忆体位置相同,而深拷贝指向不同的记忆体位置,因此有修改资料会不会影响的差异。


接下来透过程序码,针对不同的物件拷贝行为来看看是属於浅拷贝还是深拷贝!
举例: A、B、C 三人都点了拿铁,但在尺寸及其他项目上有不同的要求

浅拷贝

1. 变数赋值

使用一般的 = 赋值方式

let A = {coffee: 'Latte', size: 'L'};
let B = A;     // A 赋值给 B
B.size = 'M'   // 修改 B 的资料

console.log(B)  // {coffee: 'Latte', size: 'M'}
console.log(A)  // {coffee: 'Latte', size: 'M'},A咖啡尺寸从 L 变成 M

2. Object.assign

Object.assign 可以复制物件中的资料到另一个物件上,当物件资料只有一层时,可以做到资料不互相影响,但若结构来到两层以上时,资料还是会受彼此影响,依然属於浅拷贝。

let A = {coffee: 'Latte', size: 'L'};
let B = Object.assign({}, A);
B.size = 'M'    // b 更改尺寸为 M

console.log(B)   // {coffee: 'Latte', size: 'M'}
console.log(A)   // {coffee: 'Latte', size: 'L'},A 资料不受影响

// B 复制给 C
B.others = {sugar: 'less', shot: 3}
let C = Object.assign({},B)
C.size = 'S'
C.others.sugar = 'double'
C.others.shot = 1

console.log(C)  // {coffee: 'Latte', size: 'S', others: {sugar: 'double', shot: 1}}
console.log(B)  // {coffee: 'Latte', size: 'M', others: {sugar: 'double', shot: 1}}

// B 的 size 属於第一层资料,不受 C 影响,但是第二层的 others 资料会跟着变动

3. 展开运算子

使用 展开运算子 spread operator ... , 将物件的值存入另一个物件中,但跟 Object.assign 一样问题,当资料来到两层以上时还是浅拷贝。

let A = {coffee: 'Latte', size: 'L'};
let B = {...A}
B.size = 'M'    // 修改 B 的内容
B.others = {sugar: 'less', shot:3}  // 增加 B 的内容

console.log(A)  // {coffee: 'Latte', size: 'L'},不受影响
console.log(B)  // {coffee: 'Latte', size: 'M', others: {sugar: 'less', shot: 1}}


// 将有两层资料的 B 同样以解构赋值方式拷贝给 C
let C = {...B}
C.size = 'S'    // 修改 C 内容
C.others.sugar = 'less'
C.others.shot = 1

console.log(C)      // {coffee: 'Latte', size: 'S', others: {sugar: 'less', shot: 1}}
console.log(B)      // {coffee: 'Latte', size: 'M', others: {sugar: 'less', shot: 1}}

// B 的 size 属於第一层资料,不受 C 影响,但是第二层的 others 资料会跟着变动

深拷贝

好的,上述的展开运算子和 Object.assign 顶多可以做到一层资料的完全拷贝(如: A、B),但在巢状的资料结构,如 B、C 两人的咖啡资料怎麽做到深拷贝呢? 透过 JSON 格式!

JSON.stringify 把物件转成字串,再用 JSON.parse 把字串转回为物件。

这是真正可以做到深拷贝的方法,但是仅限 JSON 格式!

// A、B 的拷贝没问题了,直接从 B、C 拷贝试试
let B = {
coffee: 'Latte', 
size: 'L',
others: {sugar: 'less', shot: 3}
};

let C = JSON.parse(JSON.stringify(B))
C.size = 'S'
C.others.sugar = 'double'
C.others.shot = 1

console.log(B)  // {coffee: 'Latte', size: 'L',others: {sugar: 'less', shot: 3}};
console.log(C)  // {coffee: 'Latte', size: 'S',others: {sugar: 'double', shot: 1}};

// C 修改的内容并没有影响到 B,这才做到真正的深拷贝

Reference

How to differentiate between deep and shallow copies in JavaScript
深入探讨 JavaScript 中的参数传递:call by value 还是 reference?
JS 变数传递探讨:pass by value 、 pass by reference 还是 pass by sharing?
JavaScript 浅拷贝 (Shallow Copy) 与深拷贝 (Deep Copy)


<<:  Day20 :【TypeScript 学起来】是 JavaScript 没有的 Function Overloads(函式超载)

>>:  【从实作学习ASP.NET Core】Day23 | 前台 | Session 购物车 (1)

[FGL] 再探资料库 - 使用 fgldbsch 工具

Genero FGL为一个出自於资料库的语言,但怎麽和资料库搭上边的,我们还是需要来做一下理解。 ...

为了转生而点技能-JavaScript,day10(匿名函式、具名函式、立即函式

函式解构: functionName:为函式命名,当函式型态为函式陈述式,则必须命名。 函式里面宣告...

Day09 Kibana - Query DSL 复合查询

这一个章节节我们要来介绍复合查询,当单一的查询子句无法完成需求时,为了应付这种高级查询需求,所以就产...

【28】遇到不平衡资料(Imbalanced Data) 时 使用 Oversampling 解决实验

Colab连结 昨天我们使用了降低多数样本 Undersampling 的方式来解决少数样本的问题,...

为了转生而点技能-JavaScript,day8(浅笔记-物件之浅层复制与深层复制

物件复制: 浅层复制(shallow copy):仅被复制的一方能保留第一层的物件之值,但是当复制方...