iT邦幫忙

2022 iThome 鐵人賽

DAY 26
1
Modern Web

強化 JavaScript 之 - 程式語感是可以磨練成就的系列 第 26

Day26-瞭解 JS 的淺拷貝(Shallow Copy) & 深拷貝(Deep Copy)

  • 分享至 

  • xImage
  •  

前言

當初一開始在學 JS 時就常常看到淺拷貝 & 深拷貝這兩個詞,只是可惜一直沒有做個整理,所以這篇文章要來整理一下相關的觀念。

淺拷貝 & 深拷貝 是什麼?

首先我們知道 JS 變數類型分為兩大類:

1. 基本/原始型別(Primitive type): string、number、boolean、undefined、null、Symbol

原始型別都具有 passing by value 傳值特性,在複製一個變數時,是直接複製被複製變數的值。

比如當宣告一個變數 a 並為它賦值 5,若宣告變數 b 並複製變數 a,就算修改 b 的值也不會修改到 a 的值。

let a = 1;
let b = a;
b = 2;
console.log(a, b); // 1, 2

2. 物件型別(Object type): object、array、function

物件型別則具有 passing by reference 傳值特性,在複製一個變數時,是複製被複製變數的記憶體位置,而相同的記憶體位置裡面又存著變數的值。

所以當宣告一個變數 c 並為一個空物件時,若宣告變數 d 並複製變數 c,因為兩個變數參考到的記憶體位置相同,c 增加了屬性 name,d 也會跟著增加相同的屬性和屬性值。

const c = {};
const d = c;
c.name = 'Harry';
console.log(c, d); // { name: 'Harry' } { name: 'Harry' }

而上面這種單純複製記憶體位置的方式就稱為淺拷貝,而相對的,若複製一份全新的記憶體位置和值就稱為深拷貝

此外,我們知道物件或是陣列有時會有巢狀的情況,例如像物件內還有另一個物件當作屬性,或是二維、陣列內多個物件當元素等,而這些巢狀的物件若還是有參考到相同的記憶體位置,那就只能算是淺拷貝

陣列/物件的淺拷貝 & 深拷貝方式

以下介紹一些淺拷貝 & 深拷貝方式:

以下四種都是陣列的拷貝,不過都是屬於淺拷貝

const member = ['Jack', 'Mike', 'Tom', 'Marry'];

// 方法1,透過 Array.from 建立的新陣列
const memberArrayFrom = Array.from(member);
// 方法2,展開運算符
const memberSpreadParams = [...member];
// 方法3,slice()
const memberSlice = member.slice();
// 方法4,concat()
const memberConcat = [].concat(member);

以下三種則是物件的拷貝

const myDog = {
  name: 'lucky',
  age: 5
};

// 方法1,Object.assign 創造新物件
const myDogObjAssign = Object.assign({}, myDog, { name: 'puppy', age: 2 });

// 方法2,展開運算符
const myDogSpreadParams = { ...myDog, name: 'puppy', age: 2 };

// 方法3(深拷貝),JSON.parse & JSON.stringify
const myDogDeepCopy = JSON.parse(JSON.stringify(myDog));

最後一種方式 JSON.parse + JSON.stringify 可以做深拷貝,只是會有些小問題,例如以下範例中將深拷貝後的物件印出來後,可以從截圖看到一些值被改變了。

  • Date 物件變成字串
  • Infinity、NaN 變成 null
  • 正規表達式、Map、Set 等資料結構變成空物件
  • undefined 為值的屬性直接不見
const specialCase = {
  undefinedProperty: undefined,
  notANumber: NaN,
  infinityValue: Infinity,
  regExp: /^A/,
  date: new Date(2022, 9, 1),
  map: new Map(),
  set: new Set(),
};
const cloneObj = JSON.parse(JSON.stringify(specialCase));

若要使用深拷貝可以用第三方函式庫 Lodash 提供的 _.cloneDeep 函式:

import _ from "lodash";

const objects = [{ 'a': 1 }, { 'b': 2 }];
 
const deep = _.cloneDeep(objects);
console.log(deep[0] === objects[0]);
// => false

Ramda 也有類似的函式喔~

也可以透過 JS 內建的 structuredClone() 去做深拷貝:

// Create an object with a value and a circular reference to itself.
const original = { name: "MDN" };
original.itself = original;

// Clone it
const clone = structuredClone(original);

console.assert(clone !== original); // the objects are not the same (not same identity)
console.assert(clone.name === "MDN"); // they do have the same values
console.assert(clone.itself === clone); // and the circular reference is preserved

自己也能實作一個簡單版本的,如果要做的很完整當然還有些地方可優化:

const deepCopy = (inputObj) => {
  // 不是物件就直接回傳
  if (typeof inputObj !== 'object' || inputObj === null) return inputObj;
  
  // 處理特殊物件型態
  if (inputObj instanceof Date || inputObj instanceof RegExp) {
    return inputObj.constructor(inputObj);
  }
  
  const result = Array.isArray(inputObj) ? [] : {};
  for(let key in inputObj) {
    // 如果屬性也是物件,就進行遞迴
    result[key] = deepCopy(inputObj[key]);
  }

  return result;
}

const nestedArr = [[1], [2], [3]];
const copyNestedArr = deepCopy(nestedArr);

const nestedObj = {
  memberNum: 10,
  level: {
    glod: 2,
    silver: 3,
    bronze: 5,
  },
};
const copyNestedObj = deepCopy(nestedObj);

參考文章 & 推薦閱讀

JS 中的淺拷貝 (Shallow copy) 與深拷貝 (Deep copy) 原理與實作


上一篇
Day25-認識與實作 Debounce 和 Throttle
下一篇
Day27-JavaScript 的型別轉換 / == 和 === 和 Object.is() 的比較
系列文
強化 JavaScript 之 - 程式語感是可以磨練成就的30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言