淺拷貝(shallow copy)、深拷貝(deep copy) 是在 JS 中經常被提到的一個知識點,也是面試時經常考的問題之一。我在項目中經常遇到因淺拷貝而引起的 BUG,所以花了點時間來了解一下淺拷貝與深拷貝,避免之後再因為淺拷貝引起「靈異事件」。
傳值與傳址
在 JavaScript 中的內建型別主要分為基本型別以及物件型別。
- 基本型別:string, boolean, number, null, undefined, bigint, symbol 這七種
- 物件型別:除了基本型別之外的型別皆屬於物件型別,最常見比如:array。
這兩種型別之間的傳值方式並不相同:基本型別是傳值(call by value),物件型別是傳址(call by reference)
基本型別: 傳值
基本型別相互比較時會比較值本身,所以下方例子屬嚴格相等:
const a = 19; const b = 19; console.log(a === b); // true
再看另一個例子,a 為 19,並將 a 賦予給 b,b++ 會等於 20。
這是因為 number 屬於基本型別,而基本型別傳的是值,所以 b 會等於 19,19+1=20
:
const a = 19; let b = a; b++; console.log(a, b); // 19, 20
物件型別: 傳址
每個物件都是獨立存在的實體,物件之間的記憶體位址並不相同。因此在比較物件型別時比較的是記憶體位址而非值。
const a = { str: 'hello,world' }; const b = { str: 'hello,world' }; console.log(a === b); // false
並且,如果將一個物件賦予給另一個變數時,就會將物件的記憶體位址傳給該變數:
const a = { str: 'hello,world' }; let b = a; console.log(a == b); // true
所以當你在修改 b 的屬於與值時,a 也會發生變化:
let a = { str: 'hello,world' }; let b = a; b.id = 2; console.log(a, b); // { id: 2, str: "hello,world" }, {id: 2, str: "hello,world" }
淺拷貝與深拷貝
那麼什麼是淺拷貝什麼是深拷貝呢?
複製物件方式分為以下兩種:
- 淺拷貝(shallow copy):複製的是指向某個物件的指針,而不是複製物件本身,即新、舊物件還是共用同一塊記憶體位址。
- 深拷貝(deep copy):創建一個一模一樣的物件,新、舊物件不共享同一塊記憶體位址,修改物件中的屬性和值時不會修改到原來的物件。
淺拷貝的方式
Object.assign()
Object.assign()
被用來複製一個或多個物件自身所有可數的屬性到另一個目標物件。回傳的值為該目標物件。
當你的物件有多層結構時,Object.assign()
就會是淺拷貝,舉個例子:
var obj = { id: 1, data: [ { id: 2, name: 'test' } ] }; var obj2 = Object.assign({}, obj); obj2.data[0].id = 3; console.log(obj, obj2); // { id: 1, data: [{id: 3, name: 'test'}]},{id: 1,data: [{id: 3, name: 'test'}]
但要注意的是 Object.assign
在物件只有一層的時候是深拷貝:
var obj = { a: 1 }; var obj2 = Object.assign({}, obj); obj2.b = 2; console.log(obj, obj2); // { a: 1 }, { a: 1, b: 2 }
擴展運算子
還有就是擴展運算子(Spread Operator),不過跟 Object.assign()
一樣只有在多層結構時才是淺拷貝:
const obj = { group: 1, child: [ { id: 1, name: 'test' }, { id: 2, name: 'happy' } ] } const obj2 = { ...obj }; obj2.child[1].id = 3; console.log(obj, obj2); /* { group: 1, child: [{ id: 1, name: 'test' },{ id: 3, name: 'happy' }]}, { group: 1, child: [{ id: 1, name: 'test' },{ id: 3, name: 'happy' }]} */
一旦物件中只有一層結構時就會是深拷貝:
const obj = { id: 1, name: 'test' } const obj2 = { ...obj }; obj2.age = 30; console.log(obj, obj2); //{ id: 1, name: "test" }, { age: 30, id: 1, name: "test" }
其他如:concat()
, slice()
也屬於淺拷貝,這邊就不展開講了。
深拷貝的方式
如前面所說,如果物件僅只有一層時,Object.assign()
以及擴展運算子都可以實現淺拷貝。
JSON.parse 和 JSON.stringify
最常見的違深拷貝的方式,先將物件轉為字串,再將字串轉為新的物件:
const obj = { id: 1, name: 'test' } const obj2 = JSON.parse(JSON.stringify(obj)); obj2.id = 3; console.log(obj, obj2); // { id: 1, name: "test" }, { id: 3, name: "test" }
但這樣做的話,一旦遇到 function、map 以及 set…等型別時就會失效。
如果非要複製 function,可以考慮使用 prototype 而不是直接拷貝。
Object.create()
使用 Object.create()
一樣可以達到深拷貝的效果:
const obj = { id: 1, name: 'test', func: function() { return 1 } } const obj2 = Object.create(obj); obj2.func = function() { return 2 }; console.log(obj.func(), obj2.func()); // 1, 2
除此之外,lodash 這個知名的 JavaScript 函式庫也有提供
_.cloneDeep()
方法來深拷貝。
- Expo 使用 EAS build 時遇到的坑及解決方式 - 2022 年 5 月 19 日
- Typora + PicGo-Core 使用 Github 作為筆記免費圖床的詳細圖文教學 - 2022 年 5 月 12 日
- [JS學習筆記] JS 中的傳值、傳址與淺拷貝、深拷貝 - 2022 年 5 月 8 日