摘要:對象拷貝,簡而言之就是將對象再複製一份,但是,複製的方法不同將會得到不同的結果。
本文分享自華為雲社區《js對象深淺拷貝,來,試試看!》,作者: 北極光之夜。。
一.速識概念:
對象拷貝,簡而言之就是將對象再複製一份,但是,複製的方法不同將會得到不同的結果。比如直接給新變量賦值為一個對象:
// 1.建一個對象
var obj = {
name: "北極光之夜。",
like: "aurora",
};
// 2. 直接將對象賦值給變量 clone
var clone = obj;
// 3.修改obj的like屬性
obj.like = "wind";
// 4.輸出 clone 對象
console.log(clone);
從輸出結果可以看到,我明明改變的是 obj 對象的屬性,但是 clone 對象的屬性也改變了。這是因為,當創建 obj 對象時,它在堆內存中開闢了一塊空間存儲對象的內容。而當 clone 直接賦值為 obj 時,clone 並不會再重新開闢一塊堆內存,而是 obj 跟 clone 説我把我這內存空間存儲的對象的地址給你,這個地址存在棧內存中,你通過棧內存的地址找到堆內存裏對象的內容,咱們共用就完事了。所以説, obj 和 clone 指向的都是同一塊內容,不管誰改了對象的內容,別人再訪問都是改過之後的了。
所以這不是我們想要的,我不想共用,我想要屬於自己的一片天地,我命由我不由你,所以這就需要淺拷貝和深拷貝了。
簡單補充: 像一些基本數據類型的變量(Number Boolean String undefined null)被賦值時會直接在棧內存中開闢出了一個新的存儲區域用來存儲新的變量,不會如對象那樣只是把引用給別人。
二.淺拷貝原理與常用方法:
簡單來説淺拷貝就是只拷貝一層。什麼意思呢 ?比如我有一個對象 obj :
var obj = {
name: "北極光之夜。",
like: "aurora",
};
我要把它拷貝給變量 b ,原理就是我再重新開闢一塊內存,然後我直接看 obj 裏有什麼屬性和值就直接複製一份,比如通過如下方式實現:
// 1.建一個對象
var obj = {
name: "北極光之夜。",
like: "aurora",
};
// 2. 封裝一個函數,實現傳入一個對象返回一個拷貝後的新對象
function cloneObj(obj) {
let clone = {};
// 3.用 for in 遍歷obj的屬性
for (let i in obj) {
clone[i] = obj[i];
}
return clone;
}
// 4.執行函數,將得到一個新對象
var clone = cloneObj(obj);
// 5.更改 obj 屬性值
obj.like = "wind";
// 6.輸出
console.log(clone);
結果:
可以看到,就是新建一個空對象,還是循環直接賦值給它,這時改變 obj 的like屬性值 ,新建的那個對象也不受影響了。但是,如果 obj 是下面這種形式的呢:
var obj = {
name: "北極光之夜。",
like: "aurora",
num: {
a: "1",
b: "2",
},
};
此時再用上面那種方法就不行了,如果obj只改變像 name 這種屬性還沒問題,但是當 obj 改變得是像 num 這種引用類型(對象、數組都是引用類型)的數據時,拷貝的對象還是能被影響,因為淺拷貝只能拷貝一層,如果拷貝的對象裏還有子對象的話,那子對象拷貝其是也只是得到一個地址指向而已。這通過上面代碼也能看出,就一層循環而已。想要真的達到我命由我不由天的話得用深拷貝,真正的刨根問底。深拷貝見第三大點。下面介紹下淺拷貝常用的方法,當對象只有一層的時候還是用淺拷貝好。
淺拷貝常用的方法:
1.第一種是主要利用 for in 遍歷原對象的屬性。
// 封裝一個函數,實現傳入一個對象返回一個拷貝後的新對象
function cloneObj(obj) {
let clone = {};
// 用 for in 遍歷obj的屬性
for (let i in obj) {
clone[i] = obj[i];
}
return clone;
}
2.可以用Object.keys()方法:
Object.keys() 方法會返回一個由一個給定對象的自身可枚舉屬性組成的數組。
function cloneObj(obj) {
let clone = {};
for (let i of Object.keys(obj)) {
clone[i] = obj[i];
}
return clone;
}
3.可以用Object.entries()方法:
Object.entries()方法返回一個給定對象自身可枚舉屬性的鍵值對數組。
function cloneObj(obj) {
let clone = {};
for (let [key, value] of Object.entries(obj)) {
clone[key] = value;
}
return clone;
}
4.可用Object.getOwnPropertyNames()配合forEach循環:
Object.getOwnPropertyNames()返回一個由它的屬性構成的數組。
function cloneObj(obj) {
let clone = {};
Object.getOwnPropertyNames(obj).forEach(function (item) {
clone[item] = obj[item];
});
return clone;
}
5.可用Object.defineProperty()方法:
Object.defineProperty(obj, prop, descriptor) 方法會直接在一個對象上定義一個新屬性,或者修改一個對象的現有屬性,並返回此對象。obj要定義屬性的對象。prop要定義或修改的屬性的名稱或 Symbol。descriptor要定義或修改的屬性描述符。
Object.getOwnPropertyDescriptor():返回指定對象上一個自有屬性對應的屬性描述符。
屬性描述符:JS 提供了一個內部數據結構,用來描述對象的值、控制其行為。稱為屬性描述符。
function cloneObj(obj) {
let clone = {};
Object.getOwnPropertyNames(obj).forEach(function (item) {
// 獲取原本obj每個屬性修飾符
var des = Object.getOwnPropertyDescriptor(obj, item);
// 把屬性修飾符賦值給新對象
Object.defineProperty(clone, item, des);
});
return clone;
}
還有很多方法,就不一一列舉了
三.深拷貝常見方法:
深拷貝就不會像淺拷貝那樣只拷貝一層,而是有多少層我就拷貝多少層,要真正的做到全部內容都放在自己新開闢的內存裏。可以利用遞歸思想實現深拷貝。
1.可以如下實現,還是用 for in 循環,如果為屬性對象則遞歸:
function cloneObj(obj) {
let clone = {};
for (let i in obj) {
// 如果為對象則遞歸更進一層去拷貝
if (typeof obj[i] == "object" && obj[i] != null) {
clone[i] = cloneObj(obj[i]);
} else {
clone[i] = obj[i];
}
}
return clone;
}
試一試看:
// 1.建一個對象
var obj = {
name: "北極光之夜。",
like: "aurora",
age: {
a: 1,
b: 2,
},
};
// 2. 封裝一個函數,實現傳入一個對象返回一個拷貝後的新對象
function cloneObj(obj) {
let clone = {};
for (let i in obj) {
// 如果為對象則遞歸更進一層去拷貝
if (typeof obj[i] == "object" && obj[i] != null) {
clone[i] = cloneObj(obj[i]);
} else {
clone[i] = obj[i];
}
}
return clone;
}
// 4.執行函數,將得到一個新對象
var clone = cloneObj(obj);
// 5.更改 obj 屬性值
obj.age.a = "666";
// 6.輸出
console.log(clone);
結果如下,拷貝成功,原對象改變無法使新對象也改變:
2.如果對象裏面有數組怎麼辦,數組也跟對象一樣是引用類型,那麼我們可以在開頭加個判斷它是對象還是數組,數組的話賦空數組,一樣遍歷拷貝:
function cloneObj(obj) {
// 通過原型鏈判斷 obj 是否為數組
if (obj instanceof Array) {
var clone = [];
} else {
var clone = {};
}
for (let i in obj) {
// 如果為對象則遞歸更進一層去拷貝
if (typeof obj[i] == "object" && obj[i] != null) {
clone[i] = cloneObj(obj[i]);
} else {
clone[i] = obj[i];
}
}
return clone;
}
試一試看:
var obj = {
name: "北極光之夜。",
like: "aurora",
age: {
a: [1, 2, 3],
b: 2,
},
};
// 2. 封裝一個函數,實現傳入一個對象返回一個拷貝後的新對象
function cloneObj(obj) {
// 先判斷 obj 是否為數組
if (obj instanceof Array) {
var clone = [];
} else {
var clone = {};
}
for (let i in obj) {
// 如果為對象則遞歸更進一層去拷貝
if (typeof obj[i] == "object" && obj[i] != null) {
clone[i] = cloneObj(obj[i]);
} else {
clone[i] = obj[i];
}
}
return clone;
}
// 4.執行函數,將得到一個新對象
var clone = cloneObj(obj);
// 5.更改 obj 屬性值
obj.age.a[1] = "666";
// 6.輸出
console.log(clone);
結果沒問題:
當然,也可用Array.isArray(obj)方法用於判斷一個對象是否為數組。如果對象是數組返回 true,否則返回 false。
function cloneObj(obj) {
// 判斷 obj 是否為數組
if (Array.isArray(obj)) {
var clone = [];
} else {
var clone = {};
}
for (let i in obj) {
// 如果為對象則遞歸更進一層去拷貝
if (typeof obj[i] == "object" && obj[i] != null) {
clone[i] = cloneObj(obj[i]);
} else {
clone[i] = obj[i];
}
}
return clone;
}
四.總結:
以上就是深淺拷貝的大致內容啦。因為對象是引用類型,所以直接賦值對象給新變量,那麼新變量指向的內存和原對象是一樣的。所以我們通過淺拷貝和深拷貝實現開闢自己的內存空間。而淺拷貝只拷貝一層,深拷貝拷貝全部。如果,文章有什麼錯誤的,懇請大佬指出。
點擊關注,第一時間瞭解華為雲新鮮技術~