Shallow Copy 、 Deep Copy

學習Blog
6 min readSep 20, 2020

--

過去在製作MySQL種子資料時,踩到call by reference的錯誤,在此稍微做筆記。

相關背景知識:

在JS中,資料的在記憶體的貯存方式有兩種,按值(by value)與按址(by reference)

基本型別(primitive type,分別有undefined、null、string、number、boolean、symbol)是按值保存

物件(Object,不屬於基本型別的都是物件,包含Object、Array、function)則是按址

假設有個變數a指派一個基本型別,而有另一個變數b指派a時,改變b並不會影響到a

let a = 'primitive type'
let b = a
b = 'another primitive type'
console.log(a) // primitive type
console.log(b) // another primitive type

可是當a指派到物件時,b指派a再更動b的值時,a的值也會跟著修改:

const a = {key:'value'}
const b = a
console.log(a) // { key: 'value' }
b.key = 'another value'
console.log(a) // { key: 'another value' }
console.log(b) // { key: 'another value' }
可以想像物件是一個盒子,裏面的值是按值貯存,而a與b都按址指向這個物件,當藉由b改變物件內容時,a的值也跟著改變了

狀況

有兩個待做事項todo,請建立userId 1、2,讓這兩個id都有這兩個待做事項

const todos = [
{
name: "todo-1",
done: false
},
{
name: "todo-2",
done: false
}
]

指定輸出到陣列data中

data [
{ name: 'todo-1', done: false, userId: 1 },
{ name: 'todo-2', done: false, userId: 1 },
{ name: 'todo-1', done: false, userId: 2 },
{ name: 'todo-2', done: false, userId: 2 }
]

錯誤解法

for (let id = 1; id < 3; id++) {    
for (let i = 0; i < todos.length; i++) {
const newTodo = todos[i]
newTodo.userId = id
data.push(newTodo)
}
}

輸出

data [
{ name: 'todo-1', done: false, userId: 2 },
{ name: 'todo-2', done: false, userId: 2 },
{ name: 'todo-1', done: false, userId: 2 },
{ name: 'todo-2', done: false, userId: 2 }
]

每次push資料到data時,把data內結果列印出來可以看到原本的值改變了;這邊犯了一個錯誤,當要建立newTodo時,直接指派物件,而不是用淺層或深層複製

正確做法(使用展開運算子做淺層複製):

for (let id = 1; id < 3; id++) {    
for (let i = 0; i < todos.length; i++) {
const newTodo = {...todos[i]}
newTodo.userId = id
data.push(newTodo)
}
}

物件的複製方式

物件複製方式有兩種,淺層複製(shallow copy)與深層複製(deep copy),淺層是只有第一層能複製,例如前面所用的展開運算子,遇到第二層就沒有複製了

const object1 = { a: { b: 1 } }
const object2 = { ...object1 }
object2.a.b = 100
console.log(object1) //{ a: { b: 100 } }
console.log(object2) //{ a: { b: 100 } }

淺層複製有幾種:

  1. 使用展開運算子(Spread Operator)
  2. Array.concat
  3. Array.slice
  4. Object.assign

深層複製

  1. jQuery的$.extend
  2. 轉成JSON再轉回來 (JSON.parse(JSON.stringify(object))),但只有單純資料的物件才行,像是function就不能用這種方式
  3. Lodash的_.cloneDeep
  4. 手動複製(最麻煩,指派的值不能是物件)
var obj1 = { body: { a: 10 } };
var obj2 = { body: { a: obj1.body.a} };
obj2.body.a = 20;
console.log(obj1); // { body: { a: 10 } }
console.log(obj2); // { body: { a: 20 } }
console.log(obj1 === obj2); // false
console.log(obj1.body === obj2.body); // false

補充 call by sharing

有另一種情況是在function中對參數賦值,這時候便不是按址

const arr1 = {}function add(array){
array.name = 'Nya'
array = {foo:'bar'}
return array
}
const arr2 = add(arr1)
console.log(arr1) // { name: 'Nya' }
console.log(arr2) // { foo: 'bar' }

會有這樣的原因主要是在函數中重新賦值物件時,它其實會建立一個同名的新物件,等同於下列情形:

const arr1 = {}let array = arr1
array.name = 'Nya'
array = {foo:'bar'}
const arr2 = array
console.log(arr1) // { name: 'Nya' }
console.log(arr2) // { foo: 'bar' }

參考資料

關於JAVASCRIPT中的SHALLOW COPY(淺拷貝)及DEEP COPY(深拷貝)

[Javascript] 關於 JS 中的淺拷貝和深拷貝

Javascript 進階 4–9 淺層複製及深層複製

JavaScript 核心篇 學習筆記: Chap.38 — 淺層複製及深層複製

JS基本觀念:call by value 還是reference 又或是 sharing?

How to Deep Copy Objects and Arrays in JavaScript

--

--

學習Blog
學習Blog

No responses yet