談談深拷貝、淺拷貝

前提: 假設您已經知道爲什麼在JavaScript中需要深拷貝和淺拷貝了。

舉兩個例子:


const a = [1, 2, { key: 20 }]
const b = [...a]
b[2].key = 30

console.log(a[2] === b[2])


console.log(a === b) // true

const o = { k1: { kk1: 50} }

const o1 = { ...o }

o1.k1.bb = { bb: 30 }

console.log(o1.k1 === o.k1) // true

在上面數組和對象中分別改變了 b[2]o1.k1,但是最後結果的得到和原來的值保持一致。

在JavaScript中分爲2大類(原始值類型和對象類型)7中數據類型(Boolean, Null, Undefined, Number, String, Symbol),原始值類型標識對這個數據的任何操作都會返回一個新的數據,也就是說一旦申明一個原始值類型的數據則該數據不可變。如果申明一個對象類型例如: {}, new Map(), new Set(), new Regex(), new Date() 等等。再進一步來說:


(網圖,侵刪)

不同的類別的數據保存的數據結構,數據申明保存在棧數據結構中,而對象應用則分配在堆中,即用一大塊非結構化的內存區域
參考: https://developer.mozilla.org...

我們一般說的深拷貝和淺拷貝主要是對數組和對象來進行的,下面也主要對數組和對象進行實踐操作:

淺拷貝

淺拷貝可以當成是單層拷貝,何爲單層拷貝,就是複製的對象深度只有一。

數組

Array.concat

如下操作:

const arr = [1, 2, 3]
const newArr = [].concat(arr)
// arr === newArr false

上面這個方式就直接把把arr合併到新的數組中,並把新的數組返回回來,達到拷貝的目的。

Array.slice

數組的複製操作:

const arr = [1, 2, 3]
const newArr = arr.slice()
// arr === newArr false

上面從0 -> arr.length - 1 進行拷貝複製,返回一個新的數組.

Array.from

數組的創建方式,通過給定一個不定參數,然後創建一個數組

const arr = [1, 2, 3]
const newArr = Array.from(arr)
// newArr === arr false

通過原有數組創建新數組,得到拷貝目的。

總結

上面三種方式都可以簡單進行數組的淺拷貝,如果數組內嵌套有其他數據呢?這個數據是沒有處理過呢,如何做呢?且看下文

Object

Object.assign

對象合併方法:

const o = {a: 1, b: 2}
const no = Object.assign({}, o)
// o  === no1 false

通過一個新對象和原有對象合併,得到新的對象

ES6 擴展運算符(...)

擴展符號如下:

const o = { a: 1, b: 1 }
const o1 = {...o }
// o === o1 false

通過一個新的對象申明,並把原有對象屬性通過 ... 複製下來,達到拷貝目的。

深拷貝

上文中都是單層數據拷貝,在內存堆棧來說,就是在棧內重新重新開闢的空間,但是實際上,這個對象對應的二層對象並沒有進行任何處理,依舊還是原有隻想,淺拷貝實現的示意圖如下:
-w734

紅色部分是新進行申明的變量以及新的在堆中的內容,綠色部分總是沒有被複制。如何始終讓綠色可以被拷貝,被複制呢?下面就說一下這個

普通深拷貝 JSON.parse 和 JSON.stringify

通過v8提供的JSON序列化和反序列的的方法,首先把json轉換成字符串,在js中,所有Primitive 值都是不可變的,一旦修改就是新的數據。然後通過反序列的方式,直接將JSON.parse 轉換回來了即可。


const a = { ... }

const deepCloneA = JSON.parse(JSON.stringify(a))

JSON 序列化和反序列化侷限:

  1. undefined、任意的函數以及 symbol 值,在序列化過程中會被忽略(出現在非數組對象的屬性值中時)或者被轉換成 null(出現在數組中時)。
  2. 對包含循環引用的對象(對象之間相互引用,形成無限循環)執行此方法,會拋出錯誤。
  3. 所有以 symbol 爲屬性鍵的屬性都會被完全忽略掉,即便 replacer 參數中強制指定包含了它們。
  4. Date日期調用了toJSON()將其轉換爲了string字符串(同Date.toISOString()),因此會被當做字符串處理。
  5. NaN和Infinity格式的數值及null都會被當做null。
  6. 其他類型的對象,包括Map/Set/weakMap/weakSet,僅會序列化可枚舉的屬性。
https://developer.mozilla.org...

侷限也是 JSON.stringify的侷限。

那麼總結一下,如果我們要進行深拷貝,需要考慮的問題是那些呢?

  1. 對象循環拷貝,解決對象內部嵌套對象問題
  2. falsy 的數據,函數,symbol可以被拷貝, date對象能夠
  3. 循環引用的解決

解決遺留問題

對象循環拷貝

見如下代碼:

function deepCopy(o) {
  if (typeof o !== 'object') return o;

  const object = {};

  for (const key in o) {
    if (o.hasOwnProperty(key)) {
      const element = o[key];

      if (typeof element === 'object' && element !== null) deepCopy(element);
      else object[key] = element;
    }
  }
  return object
}

測試代碼:


const o = {
  a: 2,
  b: '2',
  c: { say: 'hello world' },
  d: null,
  e: undefined,
  f: function() {
    console.log('Good')
  },
  symbol: Symbol('hello')
}

console.log(o)
const o1 = deepCopy(o)
console.log(o1);

輸出如下:

{ a: 2,
  b: '2',
  c: { say: 'hello world' },
  d: null,
  e: undefined,
  f: [Function: f],
  symbol: Symbol(hello) }
{ a: 2,
  b: '2',
  d: null,
  e: undefined,
  f: [Function: f],
  symbol: Symbol(hello) }
false

在代碼裏面,我們使用遞歸,實現了基本數據的複製。上面的情況基本能夠解決我們大部分

特殊數據處理

-w378

在上述的圖中,如果我們的數據結構變成這樣,結果是怎麼樣的呢?需要對一些特別的數據進行處理, 例如Date, Map 等。這裏以Date和Map爲例子,其他類似:
-w622

最後得到兩個值都是空值,所以需要對寫類型的數據進行特別處理.

這裏增加一種工具類:

const objectTag = '[object Object]';
const arrayTag = '[object Array]';
const dateTag = '[object Date]';
const mapTag = '[object Map]';

const getTag = (o) => Object.prototype.toString.call(o)

開始真正的表演:

function deepCopy(o) {
  if (typeof o !== 'object') return o;

  const object = {};

  for (const key in o) {
    const element = o[key];

    if (element && typeof element === 'object') {
      const tag = getTag(element);
      const Ctor = element.constructor
      switch (tag) {
        case arrayTag:
        case objectTag:
          object[key] = deepCopy(element);
          break
        case dateTag:
          object[key] = new Ctor(+element)
          break
        case mapTag:
          const map = new Ctor
          element.forEach((subValue, key) => {
            map.set(key, deepCopy(subValue))
          })
          object[key] = map
        default:
          break;
      }
    } else object[key] = element;
  }
  return object;
}

運行相同的測試代碼,輸出如下:

{ a: 2,
  b: '2',
  c: { say: 'hello world' },
  d: null,
  e: undefined,
  f: [Function: f],
  g: Infinity,
  symbol: Symbol(hello),
  dd: 2019-10-22T12:54:57.976Z,
  cc: Map { 'a' => 2 } }
{ a: 2,
  b: '2',
  c: { say: 'hello world' },
  d: null,
  e: undefined,
  f: [Function: f],
  g: Infinity,
  symbol: Symbol(hello),
  dd: 2019-10-22T12:54:57.976Z,
  cc: Map { 'a' => 2 } }
false false

這裏就處理好了一些特殊數據的問題。

從上面也可以得到,一個數據的要想支持深拷貝,必須要對對應深拷貝的數據進行處理, 上面也是lodash深拷貝實現思路。

循環引用

特殊數據也處理完成後,若我們有下面數據:
-w348

直接運行看情況:
-w663

這裏需要對代碼進行一些處理,我們需要判斷代碼是否存在循環引用呢?我們在遞歸時候,不斷把當前父級(currentParent),當然的複製的數據(object), 還有最原始的數據(o)傳入,是不是可以通過循環判斷是否存在遞歸, 下面實現一下:

const objectTag = '[object Object]';
const arrayTag = '[object Array]';
const dateTag = '[object Date]';
const mapTag = '[object Map]';

const getTag = (o) => Object.prototype.toString.call(o);

function deepCopy(o, parent = null) {
  if (typeof o !== 'object') return o;

  const object = {};
  let _parent = parent
  while(_parent) {
    if (_parent.originParent === o) {
      return _parent.currentParent
    }
    _parent = _parent.parent
  }

  for (const key in o) {
    const element = o[key];

    if (element && typeof element === 'object') {
      const tag = getTag(element);
      const Ctor = element.constructor;
      switch (tag) {
        case arrayTag:
        case objectTag:
          object[key] = deepCopy(element, { parent, currentParent: object, originParent: o });
          break;
        case dateTag:
          object[key] = new Ctor(+element);
          break;
        case mapTag:
          const map = new Ctor();
          element.forEach((subValue, key) => {
            map.set(key, deepCopy(subValue, { parent, currentParent: object, originParent: o }));
          });
          object[key] = map;
        default:
          break;
      }
    } else object[key] = element;
  }
  return object;
}

我們在入口時候判斷,如果是遞歸的話,就把當前複製的結果給返回即可。查看如下示例:

const o = {
  a: 2,
  b: '2',
  c: { say: 'hello world' },
  c1: { say: 'good idea' },
  d: null,
  e: undefined,
  f: function() {
    console.log('Good');
  },
  g: Infinity,
  symbol: Symbol('hello'),
  dd: new Date(),
  cc: new Map([['a', 2]]),
};
o.ff = o;
o.cc.set('cir', o)
o.c.bb = o.c1

輸出結果:

{ a: 2,
  b: '2',
  c: { say: 'hello world', bb: { say: 'good idea' } },
  c1: { say: 'good idea' },
  d: null,
  e: undefined,
  f: [Function: f],
  g: Infinity,
  symbol: Symbol(hello),
  dd: 2019-10-24T05:17:09.550Z,
  cc: Map { 'a' => 2, 'cir' => [Circular] },
  ff: [Circular] }
{ a: 2,
  b: '2',
  c: { say: 'hello world', bb: { say: 'good idea' } },
  c1: { say: 'good idea' },
  d: null,
  e: undefined,
  f: [Function: f],
  g: Infinity,
  symbol: Symbol(hello),
  dd: 2019-10-24T05:17:09.550Z,
  cc: Map { 'a' => 2, 'cir' => [Circular] },
  ff: [Circular] }
false false
false

這樣就保證遞歸的正確性了。

閒談

或許這裏遞歸方式並不是解決重複引用的最好方法,也有方式採用 WeakMap 方式來解決,每次遞歸的時候都用WeakMap存下即可。

最後

深淺拷貝涉及JS的數據類型的存儲機制,所以對深淺拷貝可以明確區分在JS中 原始類型(Primitive) 或者 對象類型(Object) 存儲的區分。

如有問題,歡迎交流。
源碼地址: https://github.com/zsirfs/content-scripts/blob/master/deep-copy.js
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章