前提: 假設您已經知道爲什麼在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
通過一個新的對象申明,並把原有對象屬性通過 ...
複製下來,達到拷貝目的。
深拷貝
上文中都是單層數據拷貝,在內存堆棧來說,就是在棧內重新重新開闢的空間,但是實際上,這個對象對應的二層對象並沒有進行任何處理,依舊還是原有隻想,淺拷貝實現的示意圖如下:
紅色部分是新進行申明的變量以及新的在堆中的內容,綠色部分總是沒有被複制。如何始終讓綠色可以被拷貝,被複制呢?下面就說一下這個
普通深拷貝 JSON.parse 和 JSON.stringify
通過v8提供的JSON序列化和反序列的的方法,首先把json轉換成字符串,在js中,所有Primitive 值都是不可變的,一旦修改就是新的數據。然後通過反序列的方式,直接將JSON.parse 轉換回來了即可。
const a = { ... }
const deepCloneA = JSON.parse(JSON.stringify(a))
JSON 序列化和反序列化侷限:
- undefined、任意的函數以及 symbol 值,在序列化過程中會被忽略(出現在非數組對象的屬性值中時)或者被轉換成 null(出現在數組中時)。
- 對包含循環引用的對象(對象之間相互引用,形成無限循環)執行此方法,會拋出錯誤。
- 所有以 symbol 爲屬性鍵的屬性都會被完全忽略掉,即便 replacer 參數中強制指定包含了它們。
- Date日期調用了toJSON()將其轉換爲了string字符串(同Date.toISOString()),因此會被當做字符串處理。
- NaN和Infinity格式的數值及null都會被當做null。
- 其他類型的對象,包括Map/Set/weakMap/weakSet,僅會序列化可枚舉的屬性。
https://developer.mozilla.org...
侷限也是 JSON.stringify
的侷限。
那麼總結一下,如果我們要進行深拷貝,需要考慮的問題是那些呢?
- 對象循環拷貝,解決對象內部嵌套對象問題
- falsy 的數據,函數,symbol可以被拷貝, date對象能夠
- 循環引用的解決
解決遺留問題
對象循環拷貝
見如下代碼:
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