前提: 假設您已經知道爲何在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
以下操做:數組
const arr = [1, 2, 3] const newArr = [].concat(arr) // arr === newArr false
上面這個方式就直接把把arr合併到新的數組中,並把新的數組返回回來,達到拷貝的目的。數據結構
數組的複製操做:ide
const arr = [1, 2, 3] const newArr = arr.slice() // arr === newArr false
上面從0 -> arr.length - 1 進行拷貝複製,返回一個新的數組.函數
數組的建立方式,經過給定一個不定參數,而後建立一個數組工具
const arr = [1, 2, 3] const newArr = Array.from(arr) // newArr === arr false
經過原有數組建立新數組,獲得拷貝目的。
上面三種方式均可以簡單進行數組的淺拷貝,若是數組內嵌套有其餘數據呢?這個數據是沒有處理過呢,如何作呢?且看下文
對象合併方法:
const o = {a: 1, b: 2} const no = Object.assign({}, o) // o === no1 false
經過一個新對象和原有對象合併,獲得新的對象
擴展符號以下:
const o = { a: 1, b: 1 } const o1 = {...o } // o === o1 false
經過一個新的對象申明,並把原有對象屬性經過 ...
複製下來,達到拷貝目的。
上文中都是單層數據拷貝,在內存堆棧來講,就是在棧內從新從新開闢的空間,可是實際上,這個對象對應的二層對象並無進行任何處理,依舊仍是原有隻想,淺拷貝實現的示意圖以下:
紅色部分是新進行申明的變量以及新的在堆中的內容,綠色部分老是沒有被複制。如何始終讓綠色能夠被拷貝,被複制呢?下面就說一下這個
經過v8提供的JSON序列化和反序列的的方法,首先把json轉換成字符串,在js中,全部Primitive 值都是不可變的,一旦修改就是新的數據。而後經過反序列的方式,直接將JSON.parse 轉換回來了便可。
const a = { ... } const deepCloneA = JSON.parse(JSON.stringify(a))
JSON 序列化和反序列化侷限:
https://developer.mozilla.org...
侷限也是 JSON.stringify
的侷限。
那麼總結一下,若是咱們要進行深拷貝,須要考慮的問題是那些呢?
見以下代碼:
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
在代碼裏面,咱們使用遞歸,實現了基本數據的複製。上面的狀況基本可以解決咱們大部分
在上述的圖中,若是咱們的數據結構變成這樣,結果是怎麼樣的呢?須要對一些特別的數據進行處理, 例如Date, Map 等。這裏以Date和Map爲例子,其餘相似:
最後獲得兩個值都是空值,因此須要對寫類型的數據進行特別處理.
這裏增長一種工具類:
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深拷貝實現思路。
特殊數據也處理完成後,若咱們有下面數據:
直接運行看狀況:
這裏須要對代碼進行一些處理,咱們須要判斷代碼是否存在循環引用呢?咱們在遞歸時候,不斷把當前父級(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