「引言」
?臣聞求木之長者,必固其根本;欲流之遠者,必浚其泉源。
??????????????????????????????????????---- 魏征 《諫太宗十思疏》
?
或許你會問到,網上已經把深淺拷貝(算一個面試的高頻考點了吧)的文章都快寫爛了,為什么自己還要重新操刀寫一遍呢!?
?首先,一些文章,講不清也道不明本質;另外,確實有很優秀的人寫的很是生動,讓我直接看到了風景,卻不知道沿途是不是也有自己錯過的美景,唯有嘗試過,才會真正成為自己的~
?
首先,我們先來看一張筆者整理的腦圖,梳理一下~
希望通過本文的總結,你會有以下幾點收獲:
- 什么是深淺拷貝?他們與賦值有何區別?
- 淺拷貝的實現方式有哪些?
- 深拷貝的實現方式有哪些?
本章節直接從拷貝開始說起,對于基本數據類型,引用數據類型之前的區別,可以看看上面的思維導圖

引用數據類型拷貝
我們從以下三個方面來看看這塊的內容
- 賦值
- 淺拷貝
- 深拷貝
賦值
引用類型的賦值是傳址。其引用指向堆中的同一個對象,因此操作其中一個對象,另一個對象是會跟著一起變的。
舉個栗子:
let?lucy?=?{
????name:?'lucy',
????age:?23
}
let?lilei?=?lucy
lilei.name?=?'lilei'
lilei.age?=?24
console.log('lucy',?lucy)??//?lucy?{name:?"lilei",?age:?24}
console.log('lilei',?lilei)?//?lilei?{name:?"lilei",?age:?24}
上面栗子中可以看出來,修改了 lilei 的數據,lucy也會跟著變。這是初學者(筆者也曾這樣)經常犯的一個錯,后來深刻理解了對象內存的重要性!改掉了這個惡習~
那么我們該如何不讓彼此之間不影響呢?
接下來我們引出了 拷貝這個概念,拷貝又分深拷貝和淺拷貝。
來看一看具體是什么和相關區別吧。
「注意:」
- 對于基本數據類型而言,并沒有深淺拷貝的區別
- 深淺拷貝都是對于引用數據類型而言的
- 如果我們要賦值對象的所有屬性都不是引用類型時,我們可以使用淺拷貝,遍歷并復制,最后返回一個對象
「本質&使用場景」:都是復雜對象,就是說對象的屬性還是對象
淺拷貝
「本質」:只復制一層對象,當對象的屬性是引用類型時,實質復制的是其引用,當引用值指向發生改變時也會跟著改變
「原理」:遍歷并復制,最后返回一個對象
來動手實現一個簡單的淺拷貝吧
//?實現淺拷貝?for??in?
let?shallowCopy?=?(obj)?=>?{
????let?rst?=?{}
????for?(let?key?in?obj)?{
????????//?只復制本身擁有的屬性(非繼承過來的屬性)
????????if?(obj.hasOwnProperty(key))?{
????????????rst[key]?=?obj[key]
????????}
????}
????return?rst
}
let?lucy?=?{
????name:?'lucy',
????age:?23,
????hobby:?['running',?'swimming']
}
let?lilei?=?shallowCopy(lucy)
lilei.name?=?'lilei'
lilei.age?=?24
lilei.hobby[0]?=?'reading'
console.log('lucy',?lucy)
//?lucy?{name:?"lucy",?age:?23,?hobby:?['reading',?'swimming']}
console.log('lilei',?lilei)
//?lilei?{name:?"lilei",?age:?24,?hobby:?['reading',?'swimming']}
我們可以看到,當對象的屬性是引用類型時,實質復制的是其引用,當引用值指向發生改變時也會跟著改變。
深拷貝
「實質」:深拷貝出來的對象會互不影響
「原理」:對對象中子對象進行遞歸拷貝
我們下面會手寫一個深拷貝哈~接著往下看,會有不一樣的收貨!
淺拷貝的實現方式
平常用到的淺拷貝有以下幾種(歡迎評論補充,互相分享進步)
- Object.assign()
- 擴展運算符(...)
- Array.prototype.slice()
Object.assign()
首先 Object.assign(target, source)
可以把n個源對象拷貝到目標對象中去(這不是本節重點討論的內容,先一筆帶過)
然后呢,Object.assign 是 ES6新增的對象方法,那么它到底是一個深拷貝還是一個淺拷貝的方法呢?
告訴你一個絕招吧(小點聲)!
「拷貝對象時,第一級屬性是深拷貝,以后級別淺拷貝」
舉個栗子你就知道了
let?lucy?=?{
????name:?'lucy',
????age:?23,
????hobby:?['running',?'swimming']
}
let?lilei?=?Object.assign({},?lucy)
lilei.name?=?'lilei'
lilei.age?=?24
lilei.hobby[0]?=?'reading'
console.log('lucy',?lucy)
//?lucy?{name:?"lucy",?age:?23,?hobby:?['reading',?'swimming']}
console.log('lilei',?lilei)
//?lilei?{name:?"lilei",?age:?24,?hobby:?['reading',?'swimming']}
可以看出這個和咱們上面實現的那個淺拷貝的結果是一樣的。
還是那句話:「拷貝對象時,第一級屬性是深拷貝,以后級別淺拷貝」
是不是簡簡單單呢~
擴展運算符(...)
這個和 Object.assign
一樣,我們來看個栗子驗證一下
let?lucy?=?{
????name:?'lucy',
????age:?23,
????hobby:?['running',?'swimming']
}
let?lilei?=?{...lucy}
lilei.name?=?'lilei'
lilei.age?=?24
lilei.hobby[0]?=?'reading'
console.log('lucy',?lucy)
//?lucy?{name:?"lucy",?age:?23,?hobby:?['reading',?'swimming']}
console.log('lilei',?lilei)
//?lilei?{name:?"lilei",?age:?24,?hobby:?['reading',?'swimming']}
哦~一毛一樣啊和上面。
Array.prototype.slice()
說到這個方法,我第一次看見的時候是在看 vue
源碼的時候,那個時候真是漲見識(姿勢)了
話不多說,看一下就知道
//?Dep?notify?方法
Dep.prototype.notify?=?function?notify()?{
????var?subs?=?this.subs.slice()
????//?...
}
利用了slice()
方法會返回一個新的數組對象,但也是一個淺拷貝的方法。
即「拷貝對象時,第一級屬性是深拷貝,以后級別淺拷貝」
看一個具體的栗子
let?a1?=?[1,?2,?[3,?4]]
let?a2?=?a1.slice()
a2[1]?=?3
a2[2][0]?=?5
console.log('a1',?a1)?//?a1?(3)?[1,?2,?[5,?4]]
console.log('a2',?a2)?//?a2?(3)?[1,?3,?[5,?4]]
是不是驗證了這個道理呢~
同時也要去「注意」 concat
這些會返回一個新的數組對象方法等,避免造成一些工作開發者不必要的困擾~
深拷貝的實現方式
深拷貝拷貝出來的對象互不影響,但深拷貝相比于淺拷貝速度會比較慢且開銷會較大,所以考慮清楚數據結構有幾層,不是很復雜的數據結構建議淺拷貝來節省性能。
看一種最簡單的深拷貝實現方式
JSON.parse(JSON.stringify())
**原理:**能將json的值json化
就是指純JSON數據,不包含循環引用,循環引用會報錯
拿之前的栗子改造一下看看有哪些需要注意的地方
let?lucy?=?{
????name:?'lucy',
????age:?23,
????hobby:?['running',?'swimming'],
????say:?function()?{
????????return?this.name
????},
????other:?undefined
}
let?lilei?=?JSON.parse(JSON.stringify(lucy))
lilei.name?=?'lilei'
lilei.age?=?24
lilei.hobby[0]?=?'reading'
console.log('lucy',?lucy)
//?lucy?{
//????name:?'lucy',
//????age:?23,
//????hobby:?['running',?'swimming'],
//????say:?function()?{
//????????return?this.name
//?????},
//????other:?undefined
//???}
console.log('lilei',?lilei)
//?lilei?{age:?24,?hobby:?['reading',?swimming],?name:?'lilei'}
可以看出來這個方法還是挺強大的。
但是也能發現一些問題
- 會忽略 undefined ?Symbol
- 不能序列化函數
- 不能解決循環引用的對象
- 不能處理正則
- 不能正確處理 new Date() (轉換成時間戳可以拷貝)
此外,深拷貝的其他方法還有 jQuery.extend()
以及一些三方庫實現的深拷貝 lodash.cloneDeep()
等等。大家感興趣可自行了解,繼續深造~
重頭戲,面試常考,手寫一個深拷貝,哈哈哈是不是就等這個呢~
我們改造一下上面的淺拷貝
遞歸實現深拷貝
//?判斷邊界,?null?這個特殊情況
let?isObject?=?obj?=>?typeof?obj?===?'object'?&&?obj?!==?null
//?遞歸實現深拷貝
let?deepClone?=?(obj)?=>?{
????//?先判斷是數組還是對象
????let?newObj?=?Array.isArray(obj)???[]?:?{}
????if?(isObject(obj))?{
????????for?(let?key?in?obj)?{
????????????if?(obj.hasOwnProperty(key))?{
????????????????if?(isObject(obj[key]))?{
????????????????????//?遞歸調用每一層
????????????????????newObj[key]?=?deepClone(obj[key])
????????????????}?else?{
????????????????????newObj[key]?=?obj[key]
????????????????}
????????????}
????????}
????}
????return?newObj
}
let?aa?=?{
????name:?'aa',
????car:?['寶馬',?'奔馳'],
????driver:?function?()?{?},
????age:?undefined
}
let?bb?=?deepClone(aa)?//?全部拷貝了一份
bb.name?=?'bb'
bb.age?=?20
bb.driver?=?'xxx'
console.log(bb)?
//?{?name:?'bb',?car:?[?'寶馬',?'奔馳'?],?driver:?'xxx',?age:?20?}
console.log(aa)
//?{?name:?'aa',?car:?[?'寶馬',?'奔馳'?],?driver:?function()?{},?age:?undefined?}
可以看出來,咱們這個遞歸實現的深拷貝,規避掉了 上面 JSON.parse(JSON.stringify())
的一些弊端。但是還存在一些問題
- 循環檢測的問題
- 拷貝一個Symbol類型的值又該怎么解決?
- 如何解決遞歸爆棧的問題
哈希表
針對于循環檢測,我們可以使用哈希檢測的方法,比如設置一個數組或者是已經拷貝的對象,當檢測到對象已經存在哈希表時,就去除該值。
let?isObject?=?obj?=>?typeof?obj?===?'object'?&&?obj?!==?null;
let?deepClone?=?(source,?hash?=?new?WeakMap())?=>?{
????if?(!isObject(source))?return?source?//?非對象返回自身
????if?(hash.has(source))?return?hash.get(source)?//?新增檢測,?查哈希表
????let?target?=?Array.isArray(source)???[]?:?{}
????hash.set(source,?target)?//?設置哈希表值
????for?(let?key?in?source)?{
????????if?(Object.prototype.hasOwnProperty.call(source,?key))?{
????????????target[key]?=?isObject(source[key])???deepClone(source[key],?hash)?:?source[key];?//?傳入哈希表
????????}
????}
????return?target
}
let?obj?=?{
????a:?1,
????b:?{
????????c:?2,
????????d:?3
????}
}
obj.a?=?obj.b;
obj.b.c?=?obj.a;
let?clone_obj?=?deepClone(obj)
console.log(clone_obj)
上面實現有點難度,如果未能一下看透,不妨先跳過,完成之前的那個深拷貝就夠了,當然,我喜歡不懼困難的人~
剩下的兩個就交給喜歡深度思考的人來去頭腦風暴一下吧。
最后總結一下
和原數據是否指向同一個對象 | 第一層數據為基本數據類型 | 原數據中包含子對象 | |
---|---|---|---|
賦值 | 是 | 改變會影響原數據 | 改變會影響原數據 |
淺拷貝 | 否 | 改變「不會」影響原數據 | 改變會影響原數據 |
深拷貝 | 是 | 改變「不會」影響原數據 | 改變「不會」影響原數據 |
寫在最后
?享受過程帶來的喜悅,學會去克服自己的缺點!
?