1. <ins id="wdz05"><acronym id="wdz05"></acronym></ins>

          1. <menuitem id="wdz05"><video id="wdz05"></video></menuitem>
          2. <menuitem id="wdz05"></menuitem>
            <output id="wdz05"><track id="wdz05"></track></output>

            1. <ins id="wdz05"><acronym id="wdz05"></acronym></ins>
              溫馨提示×

              JavaScript中的深拷貝如何實現

              發布時間:2022-10-24 17:54:16 來源:億速云 閱讀:88 作者:iii 欄目:web開發

              今天小編給大家分享一下JavaScript中的深拷貝如何實現的相關知識點,內容詳細,邏輯清晰,相信大部分人都還太了解這方面的知識,所以分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后有所收獲,下面我們一起來了解一下吧。

              深拷貝的最終實現

              這里先直接給出最終的代碼版本,方便想快速了解的人查看,當然,你想一步步了解可以繼續查看文章余下的內容:

              function deepClone(target) {
                  const map = new WeakMap()
                  
                  function isObject(target) {
                      return (typeof target === 'object' && target ) || typeof target === 'function'
                  }
              
                  function clone(data) {
                      if (!isObject(data)) {
                          return data
                      }
                      if ([Date, RegExp].includes(data.constructor)) {
                          return new data.constructor(data)
                      }
                      if (typeof data === 'function') {
                          return new Function('return ' + data.toString())()
                      }
                      const exist = map.get(data)
                      if (exist) {
                          return exist
                      }
                      if (data instanceof Map) {
                          const result = new Map()
                          map.set(data, result)
                          data.forEach((val, key) => {
                              if (isObject(val)) {
                                  result.set(key, clone(val))
                              } else {
                                  result.set(key, val)
                              }
                          })
                          return result
                      }
                      if (data instanceof Set) {
                          const result = new Set()
                          map.set(data, result)
                          data.forEach(val => {
                              if (isObject(val)) {
                                  result.add(clone(val))
                              } else {
                                  result.add(val)
                              }
                          })
                          return result
                      }
                      const keys = Reflect.ownKeys(data)
                      const allDesc = Object.getOwnPropertyDescriptors(data)
                      const result = Object.create(Object.getPrototypeOf(data), allDesc)
                      map.set(data, result)
                      keys.forEach(key => {
                          const val = data[key]
                          if (isObject(val)) {
                              result[key] = clone(val)
                          } else {
                              result[key] = val
                          }
                      })
                      return result
                  }
              
                  return clone(target)
              }

              1. JavaScript數據類型的拷貝原理

              先看看JS數據類型圖(除了Object,其他都是基礎類型):
              JavaScript中的深拷貝如何實現
              在JavaScript中,基礎類型值的復制是直接拷貝一份新的一模一樣的數據,這兩份數據相互獨立,互不影響。而引用類型值(Object類型)的復制是傳遞對象的引用(也就是對象所在的內存地址,即指向對象的指針),相當于多個變量指向同一個對象,那么只要其中的一個變量對這個對象進行修改,其他的變量所指向的對象也會跟著修改(因為它們指向的是同一個對象)。如下圖:
              JavaScript中的深拷貝如何實現

              2. 深淺拷貝

              深淺拷貝主要針對的是Object類型,基礎類型的值本身即是復制一模一樣的一份,不區分深淺拷貝。這里我們先給出測試的拷貝對象,大家可以拿這個obj對象來測試一下自己寫的深拷貝函數是否完善:

              // 測試的obj對象
              const obj = {
                  // =========== 1.基礎數據類型 ===========
                  num: 0, // number
                  str: '', // string
                  bool: true, // boolean
                  unf: undefined, // undefined
                  nul: null, // null
                  sym: Symbol('sym'), // symbol
                  bign: BigInt(1n), // bigint
              
                  // =========== 2.Object類型 ===========
                  // 普通對象
                  obj: {
                      name: '我是一個對象',
                      id: 1
                  },
                  // 數組
                  arr: [0, 1, 2],
                  // 函數
                  func: function () {
                      console.log('我是一個函數')
                  },
                  // 日期
                  date: new Date(0),
                  // 正則
                  reg: new RegExp('/我是一個正則/ig'),
                  // Map
                  map: new Map().set('mapKey', 1),
                  // Set
                  set: new Set().add('set'),
                  // =========== 3.其他 ===========
                  [Symbol('1')]: 1  // Symbol作為key
              };
              
              // 4.添加不可枚舉屬性
              Object.defineProperty(obj, 'innumerable', {
                  enumerable: false,
                  value: '不可枚舉屬性'
              });
              
              // 5.設置原型對象
              Object.setPrototypeOf(obj, {
                  proto: 'proto'
              })
              
              // 6.設置loop成循環引用的屬性
              obj.loop = obj

              obj對象在Chrome瀏覽器中的結果:

              JavaScript中的深拷貝如何實現

              2.1 淺拷貝

              淺拷貝: 創建一個新的對象,來接受你要重新復制或引用的對象值。如果對象屬性是基本的數據類型,復制的就是基本類型的值給新對象;但如果屬性是引用數據類型,復制的就是內存中的地址,如果其中一個對象改變了這個內存中的地址所指向的對象,肯定會影響到另一個對象。

              首先我們看看一些淺拷貝的方法(詳細了解可點擊對應方法的超鏈接):

              方法使用方式注意事項
              Object.assign()Object.assign(target, ...sources)
              說明:用于將所有可枚舉屬性的值從一個或多個源對象分配到目標對象。它將返回目標對象。
              1.不會拷貝對象的繼承屬性;
              2.不會拷貝對象的不可枚舉的屬性;
              3.可以拷貝 Symbol 類型的屬性。
              展開語法let objClone = { ...obj };缺陷和Object.assign()差不多,但是如果屬性都是基本類型的值,使用擴展運算符進行淺拷貝會更加方便。
              Array.prototype.concat()拷貝數組const new_array = old_array.concat(value1[, value2[, ...[, valueN]]])淺拷貝,適用于基本類型值的數組
              Array.prototype.slice()拷貝數組arr.slice([begin[, end]])淺拷貝,適用于基本類型值的數組

              這里只列舉了常用的幾種方式,除此之外當然還有其他更多的方式。注意,我們直接使用=賦值不是淺拷貝,因為它是直接指向同一個對象了,并沒有返回一個新對象。

              手動實現一個淺拷貝:

              function shallowClone(target) {
                  if (typeof target === 'object' && target !== null) {
                      const cloneTarget = Array.isArray(target) ? [] : {};
                      for (let prop in target) {
                          if (target.hasOwnProperty(prop)) {
                              cloneTarget[prop] = target[prop];
                          }
                      }
                      return cloneTarget;
                  } else {
                      return target;
                  }
              }
              
              
              // 測試
              const shallowCloneObj = shallowClone(obj)
              
              shallowCloneObj === obj  // false,返回的是一個新對象
              shallowCloneObj.arr === obj.arr  // true,對于對象類型只拷貝了引用

              從上面這段代碼可以看出,利用類型判斷(查看typeof),針對引用類型的對象進行 for 循環遍歷對象屬性賦值給目標對象的屬性(for...in語句以任意順序遍歷一個對象的除Symbol以外的可枚舉屬性,包含原型上的屬性。查看for…in),基本就可以手工實現一個淺拷貝的代碼了。

              2.2 深拷貝

              深拷貝:創建一個新的對象,將一個對象從內存中完整地拷貝出來一份給該新對象,并從堆內存中開辟一個全新的空間存放新對象,且新對象的修改并不會改變原對象,二者實現真正的分離。

              看看現存的一些深拷貝的方法:

              方法1:JSON.stringify()

              JSON.stringfy() 其實就是將一個 JavaScript 對象或值轉換為 JSON 字符串,最后再用 JSON.parse() 的方法將JSON 字符串生成一個新的對象。(點這了解:JSON.stringfy()、JSON.parse())

              使用如下:

              function deepClone(target) {
                  if (typeof target === 'object' && target !== null) {
                      return JSON.parse(JSON.stringify(target));
                  } else {
                      return target;
                  }
              }
              
              // 開頭的測試obj存在BigInt類型、循環引用,JSON.stringfy()執行會報錯,所以除去這兩個條件進行測試
              const clonedObj = deepClone(obj)
              
              // 測試
              clonedObj === obj  // false,返回的是一個新對象
              clonedObj.arr === obj.arr  // false,說明拷貝的不是引用

              瀏覽器執行結果:

              JavaScript中的深拷貝如何實現
              從以上結果我們可知JSON.stringfy() 存在以下一些問題:

              • 執行會報錯:存在BigInt類型、循環引用。

              • 拷貝Date引用類型會變成字符串。

              • 鍵值會消失:對象的值中為Function、Undefined、Symbol 這幾種類型,。

              • 鍵值變成空對象:對象的值中為Map、Set、RegExp這幾種類型。

              • 無法拷貝:不可枚舉屬性、對象的原型鏈。

              • 補充:其他更詳細的內容請查看官方文檔:JSON.stringify()

              由于以上種種限制條件,JSON.stringfy() 方式僅限于深拷貝一些普通的對象,對于更復雜的數據類型,我們需要另尋他路。

              方法2:遞歸基礎版深拷貝

              手動遞歸實現深拷貝,我們只需要完成以下2點即可:

              • 對于基礎類型,我們只需要簡單地賦值即可(使用=)。

              • 對于引用類型,我們需要創建新的對象,并通過遍歷鍵來賦值對應的值,這個過程中如果遇到 Object 類型還需要再次進行遍歷。

              function deepClone(target) {
                  if (typeof target === 'object' && target) {
                      let cloneObj = {}
                      for (const key in target) { // 遍歷
                          const val = target[key]
                          if (typeof val === 'object' && val) {
                              cloneObj[key] = deepClone(val) // 是對象就再次調用該函數遞歸
                          } else {
                              cloneObj[key] = val // 基本類型的話直接復制值
                          }
                      }
                      return cloneObj
                  } else {
                      return target;
                  }
              }
              
              // 開頭的測試obj存在循環引用,除去這個條件進行測試
              const clonedObj = deepClone(obj)
              
              // 測試
              clonedObj === obj  // false,返回的是一個新對象
              clonedObj.arr === obj.arr  // false,說明拷貝的不是引用

              瀏覽器執行結果:

              JavaScript中的深拷貝如何實現
              該基礎版本存在許多問題:

              • 不能處理循環引用。

              • 只考慮了Object對象,而Array對象、Date對象、RegExp對象、Map對象、Set對象都變成了Object對象,且值也不正確。

              • 丟失了屬性名為Symbol類型的屬性。

              • 丟失了不可枚舉的屬性。

              • 原型上的屬性也被添加到拷貝的對象中了。

              如果存在循環引用的話,以上代碼會導致無限遞歸,從而使得堆棧溢出。如下例子:

              const a = {}
              const b = {}
              a.b = b
              b.a = a
              deepClone(a)

              對象 a 的鍵 b 指向對象 b,對象 b 的鍵 a 指向對象 a,查看a對象,可以看到是無限循環的:
              JavaScript中的深拷貝如何實現
              對對象a執行深拷貝,會出現死循環,從而耗盡內存,進而報錯:堆棧溢出
              JavaScript中的深拷貝如何實現
              如何避免這種情況呢?一種簡單的方式就是把已添加的對象記錄下來,這樣下次碰到相同的對象引用時,直接指向記錄中的對象即可。要實現這個記錄功能,我們可以借助 ES6 推出的 WeakMap 對象,該對象是一組鍵/值對的集合,其中的鍵是弱引用的。其鍵必須是對象,而值可以是任意的。(WeakMap相關見這:WeakMap)

              針對以上基礎版深拷貝存在的缺陷,我們進一步去完善,實現一個完美的深拷貝。

              方法3:遞歸完美版深拷貝

              對于基礎版深拷貝存在的問題,我們一一改進:

              存在的問題改進方案
              1. 不能處理循環引用使用 WeakMap 作為一個Hash表來進行查詢
              2. 只考慮了Object對象當參數為 Date、RegExp 、Function、Map、Set,則直接生成一個新的實例返回
              3. 屬性名為Symbol的屬性
              4. 丟失了不可枚舉的屬性
              針對能夠遍歷對象的不可枚舉屬性以及 Symbol 類型,我們可以使用 Reflect.ownKeys()
              Reflect.ownKeys(obj)相當于[...Object.getOwnPropertyNames(obj), ...Object.getOwnPropertySymbols(obj)]
              4. 原型上的屬性Object.getOwnPropertyDescriptors()設置屬性描述對象,以及Object.create()方式繼承原型鏈

              代碼實現:

              function deepClone(target) {
                  // WeakMap作為記錄對象Hash表(用于防止循環引用)
                  const map = new WeakMap()
              
                  // 判斷是否為object類型的輔助函數,減少重復代碼
                  function isObject(target) {
                      return (typeof target === 'object' && target ) || typeof target === 'function'
                  }
              
                  function clone(data) {
              
                      // 基礎類型直接返回值
                      if (!isObject(data)) {
                          return data
                      }
              
                      // 日期或者正則對象則直接構造一個新的對象返回
                      if ([Date, RegExp].includes(data.constructor)) {
                          return new data.constructor(data)
                      }
              
                      // 處理函數對象
                      if (typeof data === 'function') {
                          return new Function('return ' + data.toString())()
                      }
              
                      // 如果該對象已存在,則直接返回該對象
                      const exist = map.get(data)
                      if (exist) {
                          return exist
                      }
              
                      // 處理Map對象
                      if (data instanceof Map) {
                          const result = new Map()
                          map.set(data, result)
                          data.forEach((val, key) => {
                              // 注意:map中的值為object的話也得深拷貝
                              if (isObject(val)) {
                                  result.set(key, clone(val))
                              } else {
                                  result.set(key, val)
                              }
                          })
                          return result
                      }
              
                      // 處理Set對象
                      if (data instanceof Set) {
                          const result = new Set()
                          map.set(data, result)
                          data.forEach(val => {
                              // 注意:set中的值為object的話也得深拷貝
                              if (isObject(val)) {
                                  result.add(clone(val))
                              } else {
                                  result.add(val)
                              }
                          })
                          return result
                      }
              
                      // 收集鍵名(考慮了以Symbol作為key以及不可枚舉的屬性)
                      const keys = Reflect.ownKeys(data)
                      // 利用 Object 的 getOwnPropertyDescriptors 方法可以獲得對象的所有屬性以及對應的屬性描述
                      const allDesc = Object.getOwnPropertyDescriptors(data)
                      // 結合 Object 的 create 方法創建一個新對象,并繼承傳入原對象的原型鏈, 這里得到的result是對data的淺拷貝
                      const result = Object.create(Object.getPrototypeOf(data), allDesc)
              
                      // 新對象加入到map中,進行記錄
                      map.set(data, result)
              
                      // Object.create()是淺拷貝,所以要判斷并遞歸執行深拷貝
                      keys.forEach(key => {
                          const val = data[key]
                          if (isObject(val)) {
                              // 屬性值為 對象類型 或 函數對象 的話也需要進行深拷貝
                              result[key] = clone(val)
                          } else {
                              result[key] = val
                          }
                      })
                      return result
                  }
              
                  return clone(target)
              }
              
              
              
              // 測試
              const clonedObj = deepClone(obj)
              clonedObj === obj  // false,返回的是一個新對象
              clonedObj.arr === obj.arr  // false,說明拷貝的不是引用
              clonedObj.func === obj.func  // false,說明function也復制了一份
              clonedObj.proto  // proto,可以取到原型的屬性

              在遍歷 Object 類型數據時,我們需要把 Symbol 類型的鍵名也考慮進來,所以不能通過 Object.keys 獲取鍵名或 for...in 方式遍歷,而是通過Reflect.ownKeys()獲取所有自身的鍵名(getOwnPropertyNamesgetOwnPropertySymbols 函數將鍵名組合成數組也行:[...Object.getOwnPropertyNames(obj), ...Object.getOwnPropertySymbols(obj)]),然后再遍歷遞歸,最終實現拷貝。

              瀏覽器執行結果:
              JavaScript中的深拷貝如何實現
              可以發現我們的cloneObj對象和原來的obj對象一模一樣,并且修改cloneObj對象的各個屬性都不會對obj對象造成影響。其他的大家再多嘗試體會哦!

              以上就是“JavaScript中的深拷貝如何實現”這篇文章的所有內容,感謝各位的閱讀!相信大家閱讀完這篇文章都有很大的收獲,小編每天都會為大家更新不同的知識,如果還想學習更多的知識,請關注億速云行業資訊頻道。

              免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

              主題地圖

              成年人无码视频
              1. <ins id="wdz05"><acronym id="wdz05"></acronym></ins>

                    1. <menuitem id="wdz05"><video id="wdz05"></video></menuitem>
                    2. <menuitem id="wdz05"></menuitem>
                      <output id="wdz05"><track id="wdz05"></track></output>

                      1. <ins id="wdz05"><acronym id="wdz05"></acronym></ins>