淺談深淺拷貝|手摸手帶你入坑
前言
再次談及深拷貝,已經(jīng)過了兩三年了!花有重開日人無再少年,從當(dāng)初的懵懵懂懂到現(xiàn)在的油膩大叔,害
基本類型 與 引用類型
在這里我們先說明 基本類型 與 引用類型 的區(qū)別
基本數(shù)據(jù)類型:直接存儲在棧 (stack) 中的數(shù)據(jù)
String, Number, Boolean, Null, Undefined,Symbol
let a = 1
let b = a
b = 2
console.log(a,b)
// 1 , 2
a 與 b 變量 都是基本類型,我們直接修改 b ,a 是不會被影響到的
引用數(shù)據(jù)類型:存儲的是該對象在棧中引用,真實的數(shù)據(jù)存儲在堆(heap)
Object 、Array 、Function 、Data
題來?。?/p>
let obj = {
name: '嚴家輝',
age: 18
}
let obj1 = {
name: '嚴家輝',
age: 18
}
console.log(obj === obj1)
輸出什么?
相信有一部分同學(xué)已經(jīng)知道了是 false,那是為什么呢,真相就是引用地址不同因為是引用類型
引用類型的變量,== 和 === 只會判斷引用的地址是否相同,而不會判斷對象具體里屬性以及值是否相同
如何比較一個對象是否相等?
1.JSON.stringify
console.log(JSON.stringify(obj) === JSON.stringify(obj1))
// true
我們現(xiàn)在是將obj 轉(zhuǎn)成了 string 類型,不會對比引用地址
'{"name":"嚴家輝","age":18}' === ' {"age":18,"name":"嚴家輝"}'
但是這樣有一個問題
let obj = {
name: '嚴家輝',
age: 18
}
let obj1 = {
age: 18,
name: '嚴家輝'
}
console.log(JSON.stringify(obj) === JSON.stringify(obj1))
將 obj1 的順序修改了一遍之后?現(xiàn)在這兩個對象是否相等?還是這樣轉(zhuǎn)為字符串,肯定返回false 嘛

那么我們需要如何判斷兩個對象(不定順序的kv)是否相等呢?
let obj = {
name: '嚴家輝',
age: 18
}
let obj1 = {
age: 18,
name: '嚴家輝'
}
const isSame = (obj1, obj2) => {
var obj1keys = Object.keys(obj1);
var obj2keys = Object.keys(obj2);
if (obj2keys.length !== obj1keys.length)return false
for (let i = 0; i <= obj1keys.length - 1; i++) {
let key = obj1keys[i]
if (!obj2keys.includes(key)) return false
if (obj2[key] !== obj1[key]) return false
}
return true
}
console.log(isSame(obj,obj1)) // true
這樣對象值為基本數(shù)據(jù)類型的不管是順序是否一致都可以對比了
關(guān)于引用地址
好了,我們回過頭來看看這個
let obj = {
name: '嚴家輝',
age: 18
}
let obj1 = obj
console.log(obj === obj1)
我們在之前說過引用類型的 == 、=== 只是判斷它的引用地址是否相同
那么它在這里的引用地址肯定是一樣的,故打印 true

那么我現(xiàn)在需要修改 obj1 的 age為 24
let obj = {
name: '嚴家輝',
age: 18
}
let obj1 = obj
obj1.age = 24
console.log('obj',obj)
console.log('obj1',obj1)
輸出什么呢?

為啥obj也會改變呢
當(dāng)我們使用=將這些變量賦值到另外的變量,實際上是將對應(yīng)的值拷貝了一份,然后賦值給新的變量。
對象是通過引用傳遞,而不是值傳遞。也就是說,變量賦值只會將地址傳遞過去。
故我們修改 obj1 其實也是修改的 obj 本身

那么問題來了,假如我們在項目上面有個這樣的需求

也就是我們目標(biāo)對象 data ,a 函數(shù)先執(zhí)行,然后執(zhí)行b函數(shù),但是b函數(shù)的要用到 data.name 為嚴家輝
let data = {
name: '嚴家輝',
age: 18
}
const a = () => data.name = '老嚴'
const b = () => console.log('b函數(shù)',data.name) // 老嚴
a()
b()

在這里我們就需要了解一下深拷貝了
什么是深拷貝?

在圖中我們可以看到我們在內(nèi)存中新開了一個堆用來拷貝obj的數(shù)據(jù),但是修改obj1不會影響obj
建一個新的對象或數(shù)組,將原對象的各項屬性的“值”(數(shù)組的所有元素)拷貝過來,是“數(shù)據(jù)”而不是“引用地址” 我們希望在改變新的對象的時候,不影響原對象
怎么實現(xiàn)呢?
1、JSON 序列化
let data = {
name: '嚴家輝',
age: 18,
other: {
gender: "男"
}
}
const a = () => {
// 核心內(nèi)容
let data1 = JSON.parse(JSON.stringify(data))
data1.name = '老嚴'
data1.other.gender = '女'
console.log('a函數(shù)',data1.name)
}
const b = () => console.log('b函數(shù)',data) // 老嚴
a()
b()

先通過 JSON.stringify 將對象轉(zhuǎn)為字符串再重新序列化為對象。
這個應(yīng)該是最簡單的深拷貝了
缺點是如果對象中包含函數(shù)會丟失
let data = {
name: '嚴家輝',
age: 18,
other: {
gender: "男"
},
// + 個test函數(shù)
test: function() {}
}
const a = () => {
let data1 = JSON.parse(JSON.stringify(data))
data1.name = '老嚴'
data1.other.gender = '女'
console.log('a函數(shù)',data1)
}
const b = () => console.log('b函數(shù)',data) // 老嚴
a()
b()
看看 test 涼了沒

2、for in 遍歷
const deepCopy = obj => {
// 判斷是數(shù)組還是對象
let result = typeof obj.splice === "function" ? [] : {};
if (obj && typeof obj === 'object') {
for (let key in obj) {
if (obj[key] && typeof obj[key] === 'object') {
//如果對象的屬性值為object的時候,遞歸調(diào)用deepClone,即在吧某個值對象復(fù)制一份到新的對象的對應(yīng)值中。
result[key] = deepCopy(obj[key]);
} else {
//如果對象的屬性值不為object的時候,直接復(fù)制參數(shù)對象的每一個鍵值到新的對象對應(yīng)的鍵值對中。
result[key] = obj[key];
}
}
// 返回拷貝完成后數(shù)據(jù)
return result;
}
return obj;
}
// 使用
let data = {
name: '嚴家輝',
age: 18,
other: {
gender: "男"
}
}
const a = () => {
let data1 = deepCopy(data)
data1.name = '隔壁小花'
data1.other.gender = '女'
console.log('a函數(shù)',data1)
}
const b = () => console.log('b函數(shù)',data) // 老嚴
a()
b()
通過遍歷數(shù)據(jù)返回一個拷貝后船新的數(shù)據(jù)

缺點:據(jù)說如果數(shù)據(jù)深度 > 1000+ 會爆棧
3、lodash函數(shù)庫
使用lodash函數(shù)庫來進行深拷貝
html
<script src="https://cdn.jsdelivr.net/npm/[email protected]/lodash.min.js"></script>
js
let data = {
name: '嚴家輝',
age: 18,
other: {
gender: "男"
}
}
const a = () => {
// 核心代碼
let data1 = _.cloneDeep(data)
data1.name = '隔壁小花'
data1.other.gender = '女'
console.log('a函數(shù)',data1)
}
const b = () => console.log('b函數(shù)',data) // 老嚴
a()
b()

4、$.extend
html
<script src="http://code.jquery.com/jquery-2.1.1.min.js"></script>
js
let data = {
name: '嚴家輝',
age: 18,
other: {
gender: "男"
}
}
const a = () => {
// 核心代碼
let data1 = $.extend(true,{},data);
data1.name = '隔壁老王'
data1.other.gender = '女'
console.log('a函數(shù)',data1)
}
const b = () => console.log('b函數(shù)',data) // 老嚴
a()
b()

小結(jié)一下
推薦使用第一種或者第二種,后面兩種需要通過引入外部庫才行
其實還有其他的方法可以實現(xiàn)深拷貝,比如 Proxy 、樹遍歷等
那淺拷貝又是什么?
萬物皆有對立面,有深便有淺
剛剛我們說深拷貝是將原對象的各項屬性的“值”(數(shù)組的所有元素)拷貝過來,不是引用地址,那么淺拷貝就是 拷貝原對象的引用地址
對于淺拷貝而言,就是只拷貝對象的引用,而不深層次的拷貝對象的值,多個對象指向堆內(nèi)存中的同一對象,任何一個修改都會使得所有對象的值修改,因為它們公用一條數(shù)據(jù)
找到前面的例子和圖,這就是一個最典型的淺拷貝
let obj = {
name: '嚴家輝',
age: 18
}
let obj1 = obj
obj1.age = 24

這下明白了嗎?
啥,還不明白 ,那我們再寫倆栗子吧
淺拷貝的實現(xiàn)
1、for in
const deepCopy = obj => {
let result = typeof obj.splice === "function" ? [] : {};
if (obj && typeof obj === 'object') {
for (let key in obj) {
// 我們直接去掉遞歸,讓第二層的數(shù)據(jù)直接賦值(引用地址)
result[key] = obj[key];
}
return result;
}
return obj;
}
let data = {
name: '嚴家輝',
age: 18,
other: {
gender: "男"
}
}
const a = () => {
let data1 = deepCopy(data);
data1.name = '隔壁老王'
data1.other.gender = '女'
console.log('a函數(shù)',data1)
}
const b = () => console.log('b函數(shù)',data) // 老嚴
a()
b()

我們直接使用深拷貝的第二個實現(xiàn)方式刪除遞歸,讓它直接賦值(引用地址),這就實現(xiàn)了一個淺拷貝
2、Object.assign
let data = {
name: '嚴家輝',
age: 18,
other: {
gender: "男"
}
}
const a = () => {
// 核心代碼
let data1 = Object.assign({},data);
data1.name = '城中村村花'
data1.other.gender = '女'
console.log('a函數(shù)',data1)
}
const b = () => console.log('b函數(shù)',data) // 老嚴
a()
b()

小結(jié)一下
對于第一層的數(shù)據(jù)來說確實是拷貝成功了,但是在對象的屬性是引用類型數(shù)據(jù)時我們還是拷貝的引用地址
總結(jié)
深淺拷貝的區(qū)別就是前者將原數(shù)據(jù)重新復(fù)制了一份然后新開了一個地址,后者則是將原數(shù)據(jù)的引用地址拷貝了一份而已
如有錯誤,望各位不吝賜教。
參考文檔
https://www.jianshu.com/p/f4329eb1bace
https://segmentfault.com/a/1190000018592552?utm_source=sf-related
