深淺拷貝,溫故知新

1、深拷貝

1.1、概念

對象的深拷貝是指其屬性與其拷貝的源對象的屬性不共享相同的引用(指向相同的底層值)的副本。

因此,當你更改源或副本時,可以確保不會導致其他對象也發生更改;也就是說,你不會無意中對源或副本造成意料之外的更改。

在深拷貝中,源和副本是完全獨立的。深拷貝與其源對象不共享引用,所以對深拷貝所做的任何更改都不會影響源對象。

1.2、實現方式:

1.2.1、使用 JSON.stringify() 將該對象轉換爲 JSON 字符串,然後使用 JSON.parse() 將該字符串轉換回(全新的)JavaScript 對象。

前提:JavaScript 對象可以被序列化

序列化異常報錯

  • 存在循環引用時,會拋出異常 TypeError ("cyclic object value")(循環對象值)
  • 存在 BigInt 類型的值時,如:{ num: BigInt(1111111111) } 會拋出 TypeError ("BigInt value can't be serialized in JSON")(BigInt 值不能 JSON 序列化).

序列化需要注意的隱式轉換

  • 非數組對象的屬性不能保證以特定的順序出現在序列化後的字符串中
  • undefined、任意的函數以及 symbol 值,在序列化過程中會被忽略(出現在非數組對象的屬性值中時)或者被轉換成 null(出現在數組中時)。函數、undefined 被單獨轉換時,會返回 undefined,如 JSON.stringify(function(){}) or JSON.stringify(undefined).
  • 所有以 symbol 爲屬性鍵的屬性都會被完全忽略掉,即便 replacer 參數中強制指定包含了它們。
  • Date 日期,會調用 toJSON() 將其轉換爲 string 字符串(同 Date.toISOString())
  • NaNInfinity 格式的數值及 null 都會被當做 null
  • Map/Set/WeakMap/WeakSet,僅會序列化可枚舉的屬性
  • HTML 元素對象, 會得到 {}

示例

  // 隱式轉換

  JSON.stringify([new Number(1), new String("false"), new Boolean(false)]);
  // '[1,"false",false]'

  JSON.stringify({x: undefined, y: Object, z: Symbol("")});
  // '{}'

  JSON.stringify([undefined, Object, Symbol("")]);
  // '[null,null,null]'

  JSON.stringify({[Symbol("foo")]: "foo"});
  // '{}'

  JSON.stringify({[Symbol.for("foo")]: "foo"}, [Symbol.for("foo")]);
  // '{}'

  JSON.stringify(
      {[Symbol.for("foo")]: "foo"},
      function (k, v) {
          if (typeof k === "symbol"){
              return "a symbol";
          }
      }
  );

  // undefined

  // 不可枚舉的屬性默認會被忽略:
  JSON.stringify(
      Object.create(
          null,
          {
              x: { value: 'x', enumerable: false },
              y: { value: 'y', enumerable: true }
          }
      )
  );

  // "{"y":"y"}"

  JSON.stringify(document.body) // '{}'

  JSON.stringify(new Set([1, 2, 3])) // '{}'

  JSON.stringify(new Map([['num', 2]])) // '{}'
1.2.2、使用 window.structuredClone(),使用結構化克隆算法將給定的值進行深拷貝。

前提:可序列化的對象,且在瀏覽器環境和任何其他實現了 window 這樣全局對象的 JavaScript 運行時的環境(structuredClone() 不是 JavaScript 語言本身的特性)

優點

  • 支持把原始值中的可轉移對象轉移到新對象,而不是把屬性引用拷貝過去。
    • 可轉移對象與原始對象分離並附加到新對象; 它們不可以在原始對象中訪問被訪問到。
  • 支持循環引用

補充:結構化克隆算法

  • 結構化克隆算法是用於複製複雜 js 對象的算法。
  • 它通過遞歸輸入對象來構建克隆,同時保持先前訪問過的引用的映射,以避免無限遍歷循環。
  • Worker 的 postMessage()IndexedDB 存儲對象時內部使用該算法。所以,也可間接的通過這2個方法實現深拷貝。

補充:可轉移對象

  • 可轉移的對象(Transferable object)是擁有屬於自己的資源的對象,這些資源可以從一個上下文轉移到另一個,確保資源一次僅在一個上下文可用。傳輸後,原始對象不再可用;它不再指向轉移後的資源,並且任何讀取或者寫入該對象的嘗試都將拋出異常。
  • 可轉移對象通常用於共享資源,該資源一次僅能安全地暴露在一個 js 線程中。
  • 如:ArrayBuffer 是一個擁有內存塊的可轉移對象。當此類緩衝區(buffer)在線程之間傳輸時,相關聯的內存資源將從原始的緩衝區分離出來,並且附加到新線程創建的緩衝區對象中。原始線程中的緩衝區對象不再可用,因爲它不再擁有屬於自己的內存資源了。

異常報錯

  • Function 對象是不能被結構化克隆算法複製的,拋出 DATA_CLONE_ERR 異常,如:structuredClone(function fn() {})
  • Symbol 不能被結構化克隆算法複製的,拋出 DATA_CLONE_ERR 異常,如:structuredClone({s: Symbol(1)})
  • HTML 元素對象,拋出 DATA_CLONE_ERR 異常,如:structuredClone(document.body)

隱式轉換

  • RegExp 對象的 lastIndex 字段不會被保留
  • 屬性描述符,setters 以及 getters(以及其他類似元數據的功能)同樣不會被複制。例如,如果一個對象用屬性描述符標記爲 read-only,它將會被複製爲 read-write,因爲這是默認的情況下
  • 原形鏈上的屬性也不會被追蹤以及複製

支持的js類型

支持的 Error類型

示例

// 循環引用
const original = { name: "MDN" };
original.itself = original;

// clone
const clone = structuredClone(original);

// 驗證
console.log(clone !== original) // true
console.log(clone.name === "MDN") // true
console.log(clone.itself === clone) // true


const get = { get foo() { return 'bar' } }
console.log(get.foo) // 'bar'


class MyClass { 
  foo = 'bar' 
  myMethod() { /* ... */ }
}
const myClass = new MyClass()

const cloned = structuredClone(myClass)
// { foo: 'bar' }

cloned instanceof myClass // false
1.2.3、 使用js庫

比如 lodash 中的 cloneDeep() 方法

該方法會遞歸拷貝 value。

clone 方法參考自 結構化克隆算法 以及支持 arraysarray buffersbooleansdate objectsmapsnumbersObject 對象, regexes, sets, strings, symbols, 以及 typed arraysarguments 對象的可枚舉屬性會拷貝爲普通對象。 一些不可拷貝的對象,例如 error objectsfunctions, DOM nodes, 以及 WeakMaps 會返回空對象。

參考:MDN

2、淺拷貝

2.1、概念

對象的淺拷貝是其屬性與拷貝源對象的屬性共享相同引用(指向相同的底層值)的副本。

因此,當你更改源或副本時,也可能導致其他對象也發生更改——也就是說,你可能會無意中對源或副本造成意料之外的更改。

在淺拷貝中,對源或副本的更改可能也會導致其他對象的更改(因爲兩個對象共享相同的引用)。

2.2、實現方式

在 js 中,所有標準的內置對象進行操作:...展開語法、Array.prototype.concat()Array.prototype.slice()Array.from()Object.assign()Object.create(),創建的都是淺拷貝而不是深拷貝。

參考:MDN 淺拷貝

最後,感謝您閱讀這篇博客!希望本文能夠爲您提供有價值的信息和啓示。

如果您對本文的內容有任何疑問或建議,請隨時在評論區留言,我會盡快回復您。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章