想要講明白 JS 中物件的淺拷貝和深拷貝,需要從它的資料型別說起。
JavaScript中的資料型別
一般我們說到JS的資料型別指的是它的原始(Primitive types)資料型別(共有6種):
- String
- Number
- Boolean
- Symbol(ES6新增)
- Null
- Undefined
這些型別可以直接操作儲存在變數中的實際值。
此外,JS中還有引用資料型別:物件(Object)、陣列(Array)、函式(Function)。在JS中除了基本資料型別以外的都是物件,陣列是物件,函式是物件,正規表示式是物件,日期也是物件...
棧(stack)和堆(heap)
stack為自動分配的記憶體空間,它由系統自動釋放;而heap則是動態分配的記憶體,大小不確定。
基本資料型別是指存放在棧中的簡單資料段,資料大小確定,記憶體空間大小可以分配,它們是直接按值存放的,所以可以直接按值訪問:
var a = 10;
var b = a;
b = 20;
console.log(a); // 10
console.log(b); // 20
被重新賦值的b並沒有影響a的值。
引用型別是存放在堆記憶體中的物件,變數其實是儲存的在棧記憶體中的一個指標(儲存的是堆記憶體中的引用地址),這個指標指向堆記憶體。
var obj1 = new Object();
var obj2 = obj1;
obj2.description = "Hello World";
console.log(obj1.name); // Hello World
說明這兩個引用資料型別指向了同一個堆記憶體物件。
引用傳遞和值傳遞
在變數複製的過程中,物件的複製是引用傳遞, 基礎型別是值傳遞。
- 在將一個儲存著原始值的變數複製給另一個變數時,會將原始值的副本賦值給新變數,此後這兩個變數是完全獨立的,他們只是擁有相同的value而已。
- 引用值:在將一個儲存著物件記憶體地址的變數複製給另一個變數時,會把這個記憶體地址賦值給新變數,也就是說這兩個變數都指向了堆記憶體中的同一個物件,他們中任何一個作出的改變都會反映在另一個身上。
淺拷貝
當我們想要拷貝一個物件時,如果它的屬性是物件或陣列時,這時候我們傳遞的也只是一個地址。因此子物件在訪問該屬性時,會根據地址回溯到父物件指向的堆記憶體中,即父子物件發生了關聯,兩者的屬性值會指向同一記憶體空間。
const obj = { name: "bird", abilities: ["fly","sing"]}
function copy(target){
var objNew ={};
for (var i in target){
objNew[i] = target[i]
}
return objNew;
}
const objCopy = copy(obj)
console.log(objCopy) // { name: "bird", abilities: ["fly","sing"]}
objCopy.name = "bird01" // 修改原始型別的屬性
console.log(objCopy) // {name: 'bird01', abilities: ["fly","sing"]}
objCopy.abilities.push("lay eggs") // 修改引用型別的屬性
console.log(objCopy) // {name: 'bird01', abilities: ["fly","sing", 'lay eggs']}
console.log(obj) // {name: 'bird', abilities: ["fly","sing", 'lay eggs']}
可以看到obj的abilities屬性隨著objCopy的改變而改變,兩個物件之間發生了關聯。需要注意的是Object.assign()或者是Spread Operator都是淺拷貝:
const obj = { name: "bird", abilities: ["fly","sing"]}
const objCopy = Object.assign({}, obj) // { name: "bird", abilities: ["fly","sing"]}
objCopy.abilities.push("lay egg")
console.log(obj) // { name: "bird", abilities: ["fly","sing","lay egg"]}
const objCopy2 = {...obj} // { name: "bird", abilities: ["fly","sing","lay egg"]}
objCopy2.abilities.pop()
console.log(obj) // { name: "bird", abilities: ["fly","sing"]}
這樣的關聯往往不是我們希望看到的,那麼就有了深拷貝。
深拷貝
使用JSON.parse/stringify可以複製屬性中含有陣列的情況:
const a = { arr: ["Google","Baidu"]}
const clone = JSON.parse(JSON.stringify(a))
clone.arr.pop()
console.log(a) // { arr: ["Google","Baidu"]}
可見clone的屬性改變已經不再和a發生關聯。不過對於其他型別的屬性,在這個過程中可能會放生丟失:
const a = {
string: 'string',
number: 123,
bool: false,
nul: null,
date: new Date(), // stringified
undef: undefined, // lost
inf: Infinity, // forced to 'null'
re: /.*/, // lost
}
所以藉助一些第三方的庫是個不錯的選擇,比如:
- lodash - cloneDeep; can be imported separately via the lodash.clonedeep module and is probably your best choice if you're not already using a library that provides a deep cloning function
在不久以後也許你可以使用structuredClone
2021 update: The structuredClone global function is coming to browsers, Node.js, and Deno soon.
連結