在前端面試當中,經常會被問到淺拷貝與深拷貝的問題,這主要是考察面試者對基本數據類型和引用數據類型的理解,今天我們就通過本篇幫助大家詳細理解淺拷貝和深拷貝的概念以及實現的幾種方式。
一、認識淺拷貝和深拷貝
賦值不屬于拷貝
首先,大家需要區分,賦值不屬于拷貝:
let arr = [1,2,3]
let arr1 = arr
// 這里僅僅是把數組的內存地址賦值給arr1,這里不叫拷貝
概念
淺拷貝與深拷貝主要是作用于多層級數組或對象時存在的情況,多層級數組及對象舉例如下:
let arr = [1,2,[3,4],{n:1}] // 多層級數組,數組里還有數組或對象
let obj = {a:1,b:2,c:{d:3,e:[1,2]}} // 多層級對象,對象里還有數組或對象
(1)淺拷貝:指只對對象或數組的第一層進行復制,其他層級復制的是所存儲的內存地址。舉例如下:
let arr = [2, 3, [4, 6]]
let arr1 = [...arr] // 這里我們運用擴展運算符淺拷貝了數組arr
console.log(arr === arr1)
// false,可以看到淺拷貝的數組arr1和arr指向的是不同的內存地址
arr[0] = 0 // 這里改動原數組第一個元素
console.log(arr) // [0,3,[4,6]],原數組發生了變化
console.log(arr1) // [2,3,[4,6]],新數組無變化
console.log(arr[2] === arr1[2])
// true,但是它們的第三個元素[4,6],指向的都是同一個數組
// 這里我們修改原數組的第三個元素[4,6]的第一個元素,把4改為1
arr[2][0] = 1
console.log(arr1) // [2, 3, [1, 6]],此時打印新數組,發現它也發生了改變
通過上例可以看出淺拷貝雖然復制出了一個新的數組,但是當數組的元素為引用數據類型時,淺拷貝只拷貝了地址,通過原數組改動這個地址指向的數組,新數組同樣也會發生變化。
(2)深拷貝:會構造一個新的復合數組或對象,遇到引用所指向的引用數據類型會繼續執行拷貝。用于解決淺拷貝只能拷貝一層的情況。舉例如下:
let arr = [2, 3, [4, 6]]
let arr1 = JSON.parse( JSON.stringify(arr) )
// 通過數組轉字符串再字符串轉數組的方法進行了深拷貝
console.log(arr === arr1)
// false,可以看到深拷貝的數組arr1和arr指向的是不同的內存地址
console.log(arr[2] === arr1[2])
// false,即使是數組里第二層級的數組也是不相同
通過上例可以看出深拷貝是每一個層級都在堆內存中開辟了新的空間,是拷貝了一個全新的數組或對象,不會受原數組或原對象的影響。
二、實現淺拷貝的常用方法
方法1:通過擴展運算符實現
擴展運算符的方式既可以淺拷貝數組(上面已舉例),也可以淺拷貝對象,這里我們再舉一個淺拷貝對象的例子:
let obj = {a:1,b:2,c:{d:3,e:[1,2]}}
let obj1 = {...obj}
// 通過擴展運算符淺拷貝,獲得對象obj1
console.log(obj === obj1)
// false,obj和obj1分別指向不同的對象
console.log(obj.c === obj1.c)
// true,但是obj的c屬性的值和obj1的c屬性的值是同一個內存地址
方法2:通過Object.assign方法實現
Object.assign()方法只適用于對象,可以實現對象的合并,語法:
Object.assign(target, source_1, ..., source_n).
Object.assign()方法會將source里面的可枚舉屬性復制到target,復制的是屬性值,如果屬性值是一個引用類型,那么復制的是引用地址,因此也屬于淺拷貝。舉例如下:
let target= {
name: "小明",
}
let obj1 = {
age: 28,
sex: "男",
}
let obj2 = {
friends: ['朋友1','朋友2','朋友3'],
sayHi: function (){
console.log( 'hi' )
},
}
let obj = Object.assign(target,obj1,obj2)
console.log(obj === target) // true,因此可以用變量接收結果,也可以直接使用target
obj1.age = 30 // 把obj1的age屬性值改成30
console.log("target",target)
console.log("obj1",obj1)
上面打印結果如下:
我們可以看出返回的結果obj和target都指向淺拷貝的新對象,修改obj1的屬性age不會影響target的age屬性值。
此時給target的friends屬性添加一個新的朋友4,操作如下:
target.friends.push("朋友4")
console.log("target",target)
console.log("obj2",obj2)
我們再來看看上面的打印結果:
此時target的friends屬性和obj2的friends屬性的值指向同一個數組。
三、實現深拷貝的常用方法
方法1:通過遞歸復制所有層級實現
這里我們通過封裝一個deepClone函數來實現深層次拷貝,該方法適用于對象或數組,代碼如下:
let obj = {
name: '小明',
age: 20,
arr: [1, 2],
}
function deepClone(value) {
// 判斷傳入參數不是對象或數組時直接返回傳入的值,不再執行函數
if (typeof value !== 'object' || value == null) {
return value
}
//定義函數的返回值
let result
// 判斷傳進來的數據類型數組還是對象,對應創建新的空數組或對象
if (value instanceof Array) {
result = []
} else {
result = {}
}
// 循環遍歷拷貝
for (let key in value) {
//函數遞歸實現深層拷貝
result[key] = deepClone(value[key])
}
// 將拷貝的結果返回出去
return result
}
let newObj = deepClone(obj)
obj.arr[0] = 0 // 修改原對象的arr屬性對應的數組的元素值
console.log("obj",obj)
console.log("newObj ",newObj )
以下是上面代碼的打印結果:
我們可以看到深層遞歸的方式不會復制引用地址,所以用原對象obj修改其arr屬性對應的數組的元素,并不會影響新的對象newObj。
方法2:通過JSON對象的stringify和parse方法實現
上面我們講解深拷貝概念時用過該方法深拷貝數組,這里我們舉例來深拷貝對象:
let obj = {
name: '小明',
age: 20,
arr: [1, 2],
}
let obj1= JSON.parse( JSON.stringify(obj) )
console.log(obj.arr === obj1.arr)
// false,此時obj的arr屬性和obj1的arr屬性值不是同一個數組
通過代碼我們可以發現,JSON.stringify()方法會把obj先轉化為字符串,字符串就已經不代表任何空間地址了,就是單純的字符串,而JSON.parse()方法把字符串解析成新對象,對象的每個層級都會在堆內存中開辟新空間。
總結
JS的淺拷貝與深拷貝主要是作用于多層級數組或對象中。淺拷貝是只復制創建數組或對象的第一層,其他層級和原數組或對象擁有相同地址值,因此修改淺拷貝的數組或對象的深層的數值就會影響原數組或對象的值。而深拷貝則是拷貝一個全新的數組或對象,每一個層級都在堆內存中開辟了新的空間,和原數組或對象相互不影響。