淺拷貝和深拷貝的區(qū)別
大家都以為淺拷貝就是把引用類型的值拷貝一份法竞,實際還是引用的同一個對象,把這個叫淺拷貝,實際上這是一個大大的誤會郎楼。
- 淺拷貝和深拷貝都復(fù)制了值和地址,都是為了解決引用類型賦值后相互影響的問題窒悔。
- 但是淺拷貝只進(jìn)行一層復(fù)制呜袁,深層次的引用類型還是共享內(nèi)存地址。源對象和拷貝對象還是會相互影響
- 深拷貝就是無限層級拷貝简珠,深拷貝后的原對象不會和和拷貝對象互相影響阶界。
實現(xiàn)淺拷貝的方式有哪些呢?
- Object.assign
- 數(shù)組的slice和concat方法
- 數(shù)組靜態(tài)方法Array.from
- 擴(kuò)展運(yùn)算符
實現(xiàn)深拷貝
要求:
- 支持對象聋庵、數(shù)組膘融、日期、正則的拷貝祭玉。
- 處理Map 和 Set
- 處理原始類型(原始類型直接返回氧映,只有引用類型才有深拷貝這個概念)。
- 處理 Symbol 作為鍵名的情況脱货。
- 處理函數(shù)(函數(shù)直接返回岛都,拷貝函數(shù)沒有意義,兩個對象使用內(nèi)存中同一個地址的函數(shù)蹭劈,沒有任何問題)疗绣。
- 處理 DOM 元素(DOM 元素直接返回,拷貝 DOM 元素沒有意義铺韧,都是指向頁面中同一個)多矮。
- 額外開辟一個儲存空間 WeakMap,解決循環(huán)引用遞歸爆棧問題(引入 WeakMap 的另一個意義哈打,配合垃圾回收機(jī)制塔逃,防止內(nèi)存泄漏)。
我們要實現(xiàn)的目標(biāo):
const target = {
a: true,
b: 100,
c: 'str',
d: undefined,
e: null,
f:new Date(),
g: /abc/,
h:{
a:'ccc',
b:12
},
i:[1,2,3,4],
j:Symbol("age"),
k:new Set([1,2,3,4]),
l:new Map([[1,2],[3,4]]),
[nameSymbol]:"job",
};
target.target = target;
1.最簡單的實現(xiàn)料仗,不考慮引用類型
要拷貝的對象
const target = {
a: true,
b: 100,
c: 'str',
d: undefined,
};
要拷貝這個對象湾盗,我們只需要把對象里面的數(shù)據(jù)按個拷貝出來就可以了。
function deepClone(target){
let cloneTarget = {};
for(const key in target){
cloneTarget[key] = target[key];
}
return cloneTarget;
}
2.判斷處理 Null的情況
const target = {
a: true,
b: 100,
c: 'str',
d: undefined,
e: null,
};
我們只需要添加一個判斷就可以了立轧。
function deepClone(target){
//如果是null 就返回
if(target === null){
return target;
}
let cloneTarget = {};
for(const key in target){
cloneTarget[key] = target[key];
}
return cloneTarget;
}
3.判斷處理日期的情況
const target = {
a: true,
b: 100,
c: 'str',
d: undefined,
e: null,
f:new Date(),
};
日期是一個對象格粪,我們可以通過日期的構(gòu)造函數(shù)重新new一個對象來進(jìn)行復(fù)制
function deepClone(target){
//如果是null 就返回
if(target === null){
return target;
}
if(target instanceof Date){
return new Date(target);
}
let cloneTarget = {};
for(const key in target){
cloneTarget[key] = target[key];
}
return cloneTarget;
}
4.判斷處理正則的情況
const target = {
a: true,
b: 100,
c: 'str',
d: undefined,
e: null,
f:new Date(),
g: /abc/,
};
正則和日期一樣躏吊,同樣也可以使用構(gòu)造函數(shù)來處理。
function deepClone(target){
//如果是null 就返回
if(target === null){
return target;
}
if(target instanceof Date){
return new Date(target);
}
if(target instanceof RegExp){
return new RegExp(target);
}
let cloneTarget = {};
for(const key in target){
cloneTarget[key] = target[key];
}
return cloneTarget;
}
5.深拷貝復(fù)制引用類型
const target = {
a: true,
b: 100,
c: 'str',
d: undefined,
e: null,
f:new Date(),
g: /abc/,
h:{
a:'ccc',
b:12
}
};
里面既然有了引用類型帐萎,那么我們只需要遞歸調(diào)用一下比伏,然后返回就可以了。
arguments.callee
這里指向函數(shù)的引用疆导。
function deepClone(target){
//如果是null 就返回
if(target === null){
return target;
}
if(target instanceof Date){
return new Date(target);
}
if(target instanceof RegExp){
return new RegExp(target);
}
//處理引用類型 以免死循環(huán)
if(typeof target !== 'object'){
return target;
}
const cloneTarget = {}
for(const key in target){
cloneTarget[key] = deepClone(target[key]);
}
return cloneTarget;
}
6.處理數(shù)組的情況
const target = {
a: true,
b: 100,
c: 'str',
d: undefined,
e: null,
f:new Date(),
g: /abc/,
h:{
a:'ccc',
b:12
},
i:[1,2,3,4]
};
數(shù)組這了比較簡單赁项,它的區(qū)別僅僅只是我們拷貝的是對象還是數(shù)組。
function deepClone(target){
//如果是null 就返回
if(target === null){
return target;
}
if(target instanceof Date){
return new Date(target);
}
if(target instanceof RegExp){
return new RegExp(target);
}
//處理引用類型 以免死循環(huán)
if(typeof target !== 'object'){
return target;
}
// 處理對象和數(shù)組 以及原型鏈
const cloneTarget = new target.constructor() // 創(chuàng)建一個新的克隆對象或克隆數(shù)組
for(const key in target){
cloneTarget[key] = deepClone(target[key]);
}
return cloneTarget;
}
可以看到上面有一個騷操作澈段,就是我們通過實例的constructor
拿到它的構(gòu)造函數(shù)悠菜,然后直接new
就可以了。
這樣就不用在去判斷是否是數(shù)組還是對象败富,然后去調(diào)用它對應(yīng)的構(gòu)造函數(shù)悔醋,當(dāng)然這里這樣寫有一定的風(fēng)險。如果作為底層庫來使用囤耳,需要考慮constructor
并沒有指向它構(gòu)造函數(shù)的情況篙顺。
不過通過它的構(gòu)造函數(shù)我們解決了另外一個問題偶芍,就是原型鏈充择。通過它原本的構(gòu)造函數(shù),原型鏈自然也是完整保存的匪蟀。意想不到的小細(xì)節(jié)
7.處理Symbol的情況
const target = {
a: true,
b: 100,
c: 'str',
d: undefined,
e: null,
f:new Date(),
g: /abc/,
h:{
a:'ccc',
b:12
},
i:[1,2,3,4],
j:Symbol("age"),
[Symbol("name")]:"job",
};
Symbol
的特性就是全局唯一值椎麦,里面的參數(shù)只是一個描述符。而并不是具體值材彪。這里在處理的時候观挎,我們需要考慮兩種情況。
- 一種是作為值存在的
Symbol
- 第二種是作為鍵存在的
Symbol
作為值存在的話段化,我們可以通過Symbol.prototype.toString
方法拿到它的描述字段嘁捷。然后重新構(gòu)造一個Symbol
//處理值為Symbol的情況
if(typeof target === 'symbol'){
return Symbol(target.toString());
}
作為鍵存在的話,for in
的遍歷范圍就無法滿足我們的要求的显熏,所以需要換成遍歷范圍更加合適的Reflect.ownKeys()
雄嚣。
下面是MDN的原話
Reflect.ownKeys 方法返回一個由目標(biāo)對象自身的屬性鍵組成的數(shù)組。
它的返回值等同于Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target))喘蟆。
同時使用Reflect.ownKeys
解決了for in
的一個隱藏的問題缓升。那就是會把原型對象的屬性也遍歷下來,然后存儲到拷貝對象里面蕴轨。
升級后的效果:
function deepClone(target,map = new WeakMap()){
//如果是null 就返回
if(target === null){
return target;
}
//處理日期
if(target instanceof Date){
return new Date(target);
}
//處理正則表達(dá)式
if(target instanceof RegExp){
return new RegExp(target);
}
//處理值為Symbol的情況
if(typeof target === 'symbol'){
return Symbol(target.toString());
}
//處理引用類型 以免死循環(huán)
if(typeof target !== 'object'){
return target;
}
// 處理對象和數(shù)組
const cloneTarget = new target.constructor() // 創(chuàng)建一個新的克隆對象或克隆數(shù)組
//通過Reflect來拿到所有可枚舉和不可枚舉的屬性港谊,以及Symbol的屬性。
Reflect.ownKeys(target).forEach(key=>{
cloneTarget[key] = deepClone(target[key]);
})
return cloneTarget;
}
8.處理循環(huán)引用的情況
const target = {
a: true,
b: 100,
...
};
target.target = target;
數(shù)據(jù)是上面的這樣橙弱。這種的數(shù)據(jù)會讓遞歸進(jìn)入死循環(huán)從而造成爆棧的問題歧寺。
這里可以通過WeakMap
來建立當(dāng)前對象和拷貝對象的弱引用關(guān)系燥狰,判斷當(dāng)前對象是否存在,如果存在就使用它
保存的對象斜筐,如果不存在就添加進(jìn)去碾局。
WeakMap
的原理是,它的鍵值只能是引用類型奴艾,它的這個引用類型并不會強(qiáng)制標(biāo)記净当,當(dāng)垃圾回收機(jī)制需要釋放內(nèi)存的時候,它會被直接釋放蕴潦,而不需要做其他的操作像啼,使用者也不擔(dān)心內(nèi)存泄漏的問題。
我們用WeakMap
改造升級一下潭苞。
function deepClone(target,map = new WeakMap()){
//如果是null 就返回
if(target === null){
return target;
}
//處理日期
if(target instanceof Date){
return new Date(target);
}
//處理正則表達(dá)式
if(target instanceof RegExp){
return new RegExp(target);
}
//處理值為Symbol的情況
if(typeof target === 'symbol'){
return Symbol(target.toString());
}
//處理引用類型 以免死循環(huán)
if(typeof target !== 'object'){
return target;
}
//判斷釋放存在
if(map.has(target)){
return target;
}
// 處理對象和數(shù)組
const cloneTarget = new target.constructor() // 創(chuàng)建一個新的克隆對象或克隆數(shù)組
//保存原引用和拷貝引用的關(guān)系
map.set(target,cloneTarget)
//通過Reflect來拿到所有可枚舉和不可枚舉的屬性忽冻,以及Symbol的屬性。
Reflect.ownKeys(target).forEach(key=>{
cloneTarget[key] = deepClone(target[key],map);
})
return cloneTarget;
}
這樣就可以處理循環(huán)引用的問題了此疹。
9.處理 Set和Map的情況 和HTMLElement的情況
因為這兩個都是可迭代的數(shù)據(jù)結(jié)構(gòu)僧诚,同時它們又有自己的添加屬性的方法。所以需要按個判斷蝗碎。
由于之前 const cloneTarget = new target.constructor()
湖笨,我們并不需要去手動添加處理Map
和Set
。
而HTMLElement的情況并不需要處理蹦骑,拷貝也沒有意義慈省,直接返回就好。
function deepClone(target,map = new WeakMap()){
//如果是null 就返回
if(target === null) return target;
//處理日期
if(target instanceof Date) return new Date(target);
//處理正則表達(dá)式
if(target instanceof RegExp) return new RegExp(target);
//處理值為Symbol的情況
if(typeof target === 'symbol') return Symbol(target.toString());
// 處理 DOM元素
if (target instanceof HTMLElement) return target
//非引用類型的直接返回 比如函數(shù) 就不需要處理眠菇,直接返回就好
if(typeof target !== 'object') return target;
//從緩沖中讀取
if(map.has(target)) return target;
// 處理對象和數(shù)組
const cloneTarget = new target.constructor() // 創(chuàng)建一個新的克隆對象或克隆數(shù)組
//保存原引用和拷貝引用的關(guān)系
map.set(target,cloneTarget)
//處理Map的情況
if(target instanceof Map){
for(let [key,value] of target){
target.set(key,deepClone(value,map));
}
return target;
}
//處理set的情況
if(target instanceof Set){
for(let value of target){
target.add(deepClone(value,map))
}
return target;
}
//通過Reflect來拿到所有可枚舉和不可枚舉的屬性边败,以及Symbol的屬性。
Reflect.ownKeys(target).forEach(key=>{
cloneTarget[key] = deepClone(target[key],map);
})
return cloneTarget;
}
差不多就是這樣了捎废。我們處理了下面的情況:
const target = {
a: true,
b: 100,
c: 'str',
d: undefined,
e: null,
f:new Date(),
g: /abc/,
h:{
a:'ccc',
b:12
},
i:[1,2,3,4],
j:Symbol("age"),
k:new Set([1,2,3,4]),
l:new Map([[1,2],[3,4]]),
[nameSymbol]:"job",
};
target.target = target;
打印結(jié)果:
{
a: true,
b: 100,
c: 'str',
d: undefined,
e: null,
f: 2022-03-30T09:47:13.762Z,
g: /abc/,
h: { a: 'ccc', b: 12 },
i: [ 1, 2, 3, 4 ],
j: Symbol(Symbol(age)),
k: Set(4) { 1, 2, 3, 4 },
l: Map(2) { 1 => 2, 3 => 4 },
target: <ref *1> {
a: true,
b: 100,
//這里省略折起...
},
[Symbol(name)]: 'job'
}
如果真的要實現(xiàn)一個深拷貝 笑窜,那么情況要復(fù)雜的多,可以參考lodash
的源碼學(xué)習(xí)登疗。
聊一下另外一個深拷貝的方式: JSON.parse(JSON.stringify(target))
我們使用這個來實現(xiàn)深拷貝 排截,直接深拷貝上面的測試用例,看看能夠有幾個谜叹?
const target = {
0:NaN,
1:Infinity,
2:-Infinity,
a: true,
b: 100,
c: 'str',
d: undefined,
e: null,
f:new Date(),
g: /abc/,
h:{
a:'ccc',
b:12
},
i:[1,2,3,4],
j:Symbol("age"),
k:new Set([1,2,3,4]),
l:new Map([[1,2],[3,4]]),
[nameSymbol]:"job",
};
target.target = target;
JSON.parse(JSON.stringify(target))
//報錯:
VM8398:1 Uncaught TypeError: Converting circular structure to JSON
--> starting at object with constructor 'Object'
--- property 'target' closes the circle
at JSON.stringify (<anonymous>)
at <anonymous>:1:17
意思循環(huán)引用錯誤匾寝。我們把循環(huán)引用的代碼去掉:
target.target = target;
看一下打印出來的結(jié)果:
'0': null,
'1': null,
'2': null,
a: true
b: 100
c: "str"
e: null
f: "2022-03-30T11:20:08.362Z"
g: {}
h: {a: 'ccc', b: 12}
i: (4) [1, 2, 3, 4]
k: {}
l: {}
[[Prototype]]: Object
從頭看到尾:
-
d: undefined,
沒有了,無法處理值 為undefined
的情況 -
f:2022-03-30T11:20:08.362Z
時間變成了字符串 -
g: {}
正則表達(dá)式 沒有了 -
j:Symbol("age")
所有Symbol的值都沒有了荷腊, -
k:new Set([1,2,3,4])
Set 沒有了 -
l:new Map([[1,2],[3,4]])
Map 沒有了
會忽略的有 : undefined
,Symbol
,函數(shù)
,直接不存在
會變成對象:Map
,Set
,正則表達(dá)式
會被序列化為Null:NaN
艳悔、Infinity
、-Infinity
不能循環(huán)引用女仰。
學(xué)習(xí)參考的文章:感謝這些大佬猜年,我才能站在巨人的肩膀上抡锈。
輕松拿下 JS 淺拷貝、深拷貝
如何寫出一個驚艷面試官的深拷貝?