談談深拷貝、淺拷貝

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

舉兩個例子: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,可是最後結果的獲得和原來的值保持一致。java

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


(網圖,侵刪)git

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

咱們通常說的深拷貝和淺拷貝主要是對數組和對象來進行的,下面也主要對數組和對象進行實踐操做:github

淺拷貝

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

數組

Array.concat

以下操做:數組

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

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

Array.slice

數組的複製操做:ide

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
相關文章
相關標籤/搜索