JS複習之深淺拷貝
一、複習導論(數據類型相關)
想掌握JS的深淺拷貝,首先來回顧一下JS的數據類型,JS中數據類型分為基本數據類型和引用數據類型。
基本數據類型是指存放在棧中的簡單數據段,數據大小確定,內存空間大小可以分配,它們是直接按值存放的,所以可以直接按值訪問。包含Number、String、Boolean、null、undefined 、Symbol、bigInt。
引用類型是存放在堆內存中的對象,變量其實是保存的在棧內存中的一個指針,這個指針指向堆內存中的引用地址。除了上面的 7 種基本數據類型外,剩下的就是引用類型了,統稱為 Object 類型。細分的話,有:Object 類型、Array 類型、Date 類型、RegExp 類型、Function 類型 等

由於基本數據類型和引用數據類型存儲方式的差異,所以我們在進行複製變量時,基本數據類型複製後會產生兩個獨立不會互相影響的變量,而引用數據類型複製時,實際上是將這個引用類型在棧內存中的引用地址複製了一份給新的變量,其實就是一個指針。因此當操作結束後,這兩個變量實際上指向的是同一個在堆內存中的對象,改變其中任意一個對象,另一個對象也會跟着改變。於是在引用數據類型的複製過程中便出現了深淺拷貝的概念。
二、深淺拷貝的區別
淺拷貝,對於目標對象第一層為基本數據類型的數據,就是直接賦值,即傳值;而對於目標對象第一層為引用數據類型的數據,就是直接賦存於棧內存中的堆內存地址,即傳地址,並沒有開闢新的棧,也就是複製的結果是兩個對象指向同一個地址,修改其中一個對象的屬性,則另一個對象的屬性也會改變。
深拷貝,則是開闢新的棧,兩個對象對應兩個不同的地址,修改一個對象的屬性,不會改變另一個對象的屬性。
三、淺拷貝的實現方式
1.對象的淺拷貝
(1)Object.assign()
ES6中新增的方法,用於對象的合併,將源對象(source)的所有可枚舉屬性,複製到目標對象(target),詳細用法傳送門。代碼示例:
let obj = {
a:1,
b:{
m:'2',
n:'3'
},
c:[1,2,3,4,5,6]
}
let copyObj = Object.assign({},obj)
obj.a = 5
obj.b.m = '222'
console.log(copyObj) //{ a: 1, b: { m: '222', n: '3' }, c: [ 1, 2, 3, 4, 5, 6 ] }
上面的代碼修改了obj內部a的值和b.m的值,但是在複製出來的對象中,a的值並未改變,m的值改變了。所以Object.assign()複製時遇到基本數據類型時直接複製值,但是遇到引用數據類型仍然複製的是地址,嚴格來講屬於淺拷貝。
(2)循環遍歷
let obj = {
a:1,
b:{
m:'2',
n:'3'
},
c:[1,2,3,4,5,6]
}
let copyObj = {}
for(var k in obj){
copyObj[k] = obj[k]
}
copyObj.a = 5
copyObj.b.m = '333'
console.log(obj) //{ a: 1, b: { m: '333', n: '3' }, c: [ 1, 2, 3, 4, 5, 6 ] }
console.log(copyObj) //{ a: 5, b: { m: '333', n: '3' }, c: [ 1, 2, 3, 4, 5, 6 ] }
2.數組的淺拷貝
Array.concat()、Array.slice(0)、 Array.from()、拓展運算符(…)
let arr = [1,2,3,4,[5,6]] let copyArr = arr.concat() //arr.slice(0) 、Array.from()、[...arr] copyArr[0] = '555' copyArr[4][1] = 7 console.log(arr) //[ 1, 2, 3, 4, [ 5, 7 ] ] console.log(copyArr) //[ '555', 2, 3, 4, [ 5, 7 ] ]
四、深拷貝的實現方式
1.JSON.parse()和JSON.stringify()
const obj1 = {
x: 1,
y: {
m: 1
}
};
const obj2 = JSON.parse(JSON.stringify(obj1));
console.log(obj1) //{x: 1, y: {m: 1}}
console.log(obj2) //{x: 1, y: {m: 1}}
obj2.y.m = 2; //修改obj2.y.m
console.log(obj1) //{x: 1, y: {m: 1}} 原對象未改變
console.log(obj2) //{x: 2, y: {m: 2}}
這種方法使用較為簡單,可以滿足基本日常的深拷貝需求,而且能夠處理JSON格式能表示的所有數據類型,但是有以下幾個缺點:
(1)undefined、任意的函數、正則表達式類型以及 symbol 值,在序列化過程中會被忽略(出現在非數組對象的屬性值中時)或者被轉換成 null(出現在數組中時);
(2) 它會拋棄對象的constructor。也就是深拷貝之後,不管這個對象原來的構造函數是什麼,在深拷貝之後都會變成Object;
(3) 對於正則表達式類型、函數類型等無法進行深拷貝(而且會直接丟失相應的值)
(4) 如果對象中存在循環引用的情況無法正確處理。
//忽略undefined、symbol 和函數
let obj = {
name: 'muyiy',
a: undefined,
b: Symbol('muyiy'),
c: function() {}
}
console.log(obj);
// {
// name: "muyiy",
// a: undefined,
// b: Symbol(muyiy),
// c: ƒ ()
// }
let b = JSON.parse(JSON.stringify(obj));
console.log(b);// {name: "muyiy"}
//循環引用情況下,會報錯
let obj = {
a: 1,
b: {
c: 2,
d: 3
}
}
obj.a = obj.b;
obj.b.c = obj.a;
let b = JSON.parse(JSON.stringify(obj));// Uncaught TypeError: Converting circular structure to JSON
//正則情況下
let obj = {
name: "muyiy",
a: /'123'/
}
console.log(obj);
// {name: "muyiy", a: /'123'/}
let b = JSON.parse(JSON.stringify(obj));
console.log(b);
2.jQuery.extend()
附上源碼解析:
jQuery.extend = jQuery.fn.extend = function() { //給jQuery對象和jQuery原型對象都添加了extend擴展方法
var options, name, src, copy, copyIsArray, clone, target = arguments[0] || {},
i = 1,
length = arguments.length,
deep = false;
//以上其中的變量:options是一個緩存變量,用來緩存arguments[i],name是用來接收將要被擴展對象的key,src改變之前target對象上每個key對應的value。
//copy傳入對象上每個key對應的value,copyIsArray判定copy是否為一個數組,clone深拷貝中用來臨時存對象或數組的src。
// 處理深拷貝的情況
if (typeof target === "boolean") {
deep = target;
target = arguments[1] || {};
//跳過布爾值和目標
i++;
}
// 控制當target不是object或者function的情況
if (typeof target !== "object" && !jQuery.isFunction(target)) {
target = {};
}
// 當參數列表長度等於i的時候,擴展jQuery對象自身。
if (length === i) {
target = this; --i;
}
for (; i < length; i++) {
if ((options = arguments[i]) != null) {
// 擴展基礎對象
for (name in options) {
src = target[name];
copy = options[name];
// 防止永無止境的循環,這裡舉個例子,
// 如 var a = {name : b};
// var b = {name : a}
// var c = $.extend(a, b);
// console.log(c);
// 如果沒有這個判斷變成可以無限展開的對象
// 加上這句判斷結果是 {name: undefined}
if (target === copy) {
continue;
}
if (deep && copy && (jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)))) {
if (copyIsArray) {
copyIsArray = false;
clone = src && jQuery.isArray(src) ? src: []; // 如果src存在且是數組的話就讓clone副本等於src否則等於空數組。
} else {
clone = src && jQuery.isPlainObject(src) ? src: {}; // 如果src存在且是對象的話就讓clone副本等於src否則等於空數組。
}
// 遞歸拷貝
target[name] = jQuery.extend(deep, clone, copy);
} else if (copy !== undefined) {
target[name] = copy; // 若原對象存在name屬性,則直接覆蓋掉;若不存在,則創建新的屬性。
}
}
}
}
// 返回修改的對象
return target;
};
jQuery的extend方法使用基本的遞歸思路實現了淺拷貝和深拷貝,但是這個方法也無法處理源對象內部循環引用。
3.lodash.cloneDeep()
已經有大佬專門寫了lodash的源碼解析(傳送門)
4.自己實現一個深拷貝
function deepClone(source) {
if (!source && typeof source !== 'object') {
throw new Error('error arguments', 'deepClone')
}
const targetObj = source.constructor === Array ? [] : {}
Object.keys(source).forEach(keys => {
if (source[keys] && typeof source[keys] === 'object') {
targetObj[keys] = deepClone(source[keys])
} else {
targetObj[keys] = source[keys]
}
})
return targetObj
}
參考文檔://segmentfault.com/a/1190000015042902
//github.com/yygmind/blog/issues/29


