JavaScript淺拷貝和深拷貝

老毛發表於2020-12-30
想要講明白 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.
連結

相關文章