[分享] 簡單理解 call by reference 與 Deep Clone

Lastor
Code 隨筆放置場
6 min readAug 12, 2019

這篇的專有名詞比較多, 而且是蠻重要的部分。
對於這塊不太熟的同學, 可能會被名詞搞得很繞。

分享一下個人的理解方式:
其實給 Obj 賦值, 就是對 Obj 貼上一張寫著 “別稱” 的貼紙

const a = ['a', 'b']  // 在這個 [] 框框上, 貼上貼紙, 寫著a
const b = a // 在貼了a的框框上, 再貼上一張貼紙, 寫著b

被宣告成 a 和 b 的東西, 只是那個 “框”, 而不是裡面的內容。
所以a 和 b 現在指的是同一個框, 它們理所當然共享裡面的內容。

b[0] = 0
b // [0, 'b']
a // [0, 'b'], a跟b是同一個 "框"

這也是為什麼用 const 宣物件, 內容改變也不會報錯的原因,
因為你只是把內容換掉, 外面的 “框” 並沒有更動到。

const array = ['a', 'b']
array.push('c')
array // ['a', 'b', 'c'], 沒報錯
array = [0, 1] // Error, 因為把它本來的 "框" 給換掉了

Deep Clone(深拷貝) & Shallow Clone(淺拷貝)

只是copy了 “框” 的行為, 稱做淺拷貝。
就像上述的 b = a 一樣, 如果改變了其中一個的內容, 另一個也會變。
而這通常不會是我們所期望的。

深拷貝則是, 真的去做出一個新的 “框”, 然後把值copy進去。

const array = ['a', 'b']
const clone = Object.assign([], array) // 宣告一個新的"框", 把值轉進去
clone[0] = 0
clone // [0, 'b']
array // ['a', 'b'], clone是新的 "框" 它們已不再共享內容

但是!! 這種情況直接用 Object.assign() , 就沒法完成預期的深拷貝。

const array = [ {name: 'Tom'}, {name: 'John'} ]
const clone = Object.assign([], array)
clone[0].name = 'Jack'
clone // [ {name: 'Jack'}, {name: 'John'} ]
array // [ {name: 'Jack'}, {name: 'John'} ], 深拷貝失敗...

這是因為我們只做了新的 [ ] 這個框, 可是裡面的內容是裝在 { } 這個框。
clone現在雖然與array是不同的 [ ] 外框, 但裡面的 { } 花框仍然是同一個,
所以 { } 花框裡面的內容, 兩者仍然是共用的。

要深拷貝這種 Obj 其實很簡單, 就是利用迭代方法往裡面挖一層, 去 assign 那個 { } 花框。

const array = [ {name: 'Tom'}, {name: 'John'} ]
const clone = array.map(item => Object.assign({}, item))
clone[0].name = 'Jack'
clone // [ {name: 'Jack'}, {name: 'John'} ]
array // [ {name: 'Tom'}, {name: 'John'} ], 沒有同步改變了!!

所以, 這整個概念其實是很單純的, 要注意的是那個 “框” 而不是值。
上面這段code, 一些細心的人可能會注意到, 我們只做了新的 { } 花框,
外層的 [ ] 框 不用新做嗎?

答案是不用, 因為上面的操作同時也產生了新的 [ ] 框, 它是由map()產生的。
map() 迭代時, 會回傳一個新array, 也就是一個新 [ ] 框。
所以不用擔心這種操作。

clone[0] = 0
clone // [ 0, {name: 'John'} ]
array // [ {name: 'Tom'}, {name: 'John'} ], 外層的[] 也有被新做

最後分享一個小技巧,
如果只是單純包值的array要clone, 不使用 Object.assign() 也可以。
可以使用 slice()

const array = ['a', 'b']
const clone = array.slice(0) // slice會回傳一個"新array", 新的[]框
clone[0] = 0
clone // [0, 'b']
array // ['a', 'b']

【同學提問】item是key值? item是否可以替代成其他我們理解或想要使用的其他代號?

const clone = array.map(item => Object.assign({}, item))

回答:

這個map()裡面的item, 是一個可自定義名稱的參數。
map()開始運作時, 會傳進item的東西, 痾…. 這好像叫引數來著?
反正就是會傳進item這個參數的值, 不是Obj的keys, 而是values。
如同 forEash() 或是 for-of 一樣, 都是迭代 values。

只是因為我不想寫太長, 所以用單行箭頭函式, 並把花框給拿掉。
換回function的話是這樣。

const array = [{ name: 'Tom' }, { name: 'John' }]
array.map(function(item) {
return Object.assign({}, item)
})
// [{ name: 'Tom' }, { name: 'John' }]

必須要加上return, 不然map()接到的回傳值會是 undefined。
箭頭函式也是, 如果加上了花框, 就必須要加 return。

const array = [{ name: 'Tom' }, { name: 'John' }]
array.map(function(item) { Object.assign({}, item) })
// [undefined, undefined]
array.map(item => { Object.assign({}, item) })
// [undefined, undefined]

這是因為花框聲明了這裡面是 Local 作用域, 如果不寫 return,
花框裡面發生的事情, 外面世界的 map() 是接收不到的。

當然迭代器也可以用 for-of , 寫詳細一點的話會像是這樣

const array = [{ name: 'Tom' }, { name: 'John' }]
const clone = []
for (const item of array) {
// 新的{}花框先用一個容器保存
const temp = {}
// 把array的value, 也就是裡面的 { Obj } 給assign進去
Object.assign(temp, item)
// 推回clone
clone.push(temp)
}

只是這樣寫真的太長了, 搭配 map() 以及箭頭函式, 一行搞定

const array = [{ name: 'Tom' }, { name: 'John' }]
const clone = array.map(item => Object.assign({}, item))

助教補充

(前略)
一般來說深層複製會使用JSON.parse(JSON.stringify(item)),或是lodash函式庫也有提供一個cloneDeep方法,要視狀況來處理。

--

--

Lastor
Code 隨筆放置場

Web Frontend / 3D Modeling / Game and Animation. 設計本科生,前遊戲業 3D Artist,專擅日本動畫與遊戲相關領域。現在轉職為前端工程師,以專業遊戲美術的角度涉足 Web 前端開發。