常見淺拷貝和深拷貝

一. 解構賦值

最近在使用Redux想到一個問題,Redux裏常用的一種語法是這樣的:

setSth(state, { payload }) {
    const { newthing} = payload;
    return {
        ...state,
        newthing
    }
}

Redux通過解構賦值…state,保留了state裏未修改的部分,並覆蓋修改的部分,那麼現在問題來了,這裏的新對象通過解構得來的…state,是否是原來屬性的引用賦值呢?

我們知道,如下情況:

let state = {a: 1, b: 2}
let state2 = state;
state2.a = 5;
// state.a === 5 => true

這裏state2是state的一個引用賦值。
那麼使用解構賦值生成一個新對象會如何呢?

let state = {a: 1, b: 2}
let state2 = { ...state }
state2.a = 5;
// state.a === 1 => true

通過實驗可見,state2屬性的修改並沒有影響到state,所以這是一份拷貝,那麼問題又來了,這個拷貝是隻有一層的淺拷貝,還是遞歸進去的深拷貝呢,我直覺地認爲是深拷貝,於是又做了一個實驗:

let state = {a: {a1: 1, a2: 2}, b: 2}
let state2 = { ...state }
state2.a.a1 = 5;
// state.a.a1 === 5 => true

事實證明我的直覺錯了,解構複製到全新對象裏只是淺拷貝,對象裏屬性的屬性還是對原來對象裏屬性的屬性的一個引用,因爲解構賦值可以生成一份淺拷貝,其實我們針對已知結構的對象也可以全部使用這個方法實現自己想要的拷貝。比如上例中:

let state = {a: {a1: 1, a2: 2}, b: 2}
let state2 = { ...state, a: { ...state.a } }

總結: 淺拷貝是個籠統說法,只要深層屬性值指向同一個引用,都叫淺拷貝,所以解構賦值就是淺拷貝。深拷貝是完全的拷貝。

二. Object.assign()

Object.assign() 方法用於將所有可枚舉屬性的值從一個或多個源對象複製到目標對象,它將返回目標對象。

// 木易楊
let a = {
    name: "muyiy",
    book: {
        title: "You Don't Know JS",
        price: "45"
    }
}
let b = Object.assign({}, a);
console.log(b);
// {
// 	name: "muyiy",
// 	book: {title: "You Don't Know JS", price: "45"}
// } 

a.name = "change";
a.book.price = "55";
console.log(a);
// {
// 	name: "change",
// 	book: {title: "You Don't Know JS", price: "55"}
// } 

console.log(b);
// {
// 	name: "muyiy",
// 	book: {title: "You Don't Know JS", price: "55"}
// } 

面代碼改變對象 a 之後,對象 b 的基本屬性保持不變。但是當改變對象 a 中的對象 book 時,對象 b 相應的位置也發生了變化。

三. 語法展開(同上述對象解構)

// 木易楊
let a = {
    name: "muyiy",
    book: {
        title: "You Don't Know JS",
        price: "45"
    }
}
let b = {...a};
console.log(b);
// {
// 	name: "muyiy",
// 	book: {title: "You Don't Know JS", price: "45"}
// } 

a.name = "change";
a.book.price = "55";
console.log(a);
// {
// 	name: "change",
// 	book: {title: "You Don't Know JS", price: "55"}
// } 

console.log(b);
// {
// 	name: "muyiy",
// 	book: {title: "You Don't Know JS", price: "55"}
// } 

通過代碼可以看出實際效果和 Object.assign() 是一樣的。

四. Array.prototype.slice()

slice() 方法返回一個新的數組對象,這一對象是一個由 begin和 end(不包括end)決定的原數組的淺拷貝。原始數組不會被改變。

// 木易楊
let a = [0, "1", [2, 3]];
let b = a.slice(1);
console.log(b);
// ["1", [2, 3]]

a[1] = "99";
a[2][0] = 4;
console.log(a);
// [0, "99", [4, 3]]

console.log(b);
//  ["1", [4, 3]]

可以看出,改變a[1]之後b[0] 的值並沒有發生變化,但改變 a[2][0]之後,相應的 b[1][0]的值也發生變化。說明slice()方法是淺拷貝,相應的還有concat等,在工作中面對複雜數組結構要額外注意。


上述均爲淺拷貝,下面說一說如何實現深拷貝

一. JSON.parse(JSON.stringify(object))

// 木易楊
let a = {
    name: "muyiy",
    book: {
        title: "You Don't Know JS",
        price: "45"
    }
}
let b = JSON.parse(JSON.stringify(a));
console.log(b);
// {
// 	name: "muyiy",
// 	book: {title: "You Don't Know JS", price: "45"}
// } 

a.name = "change";
a.book.price = "55";
console.log(a);
// {
// 	name: "change",
// 	book: {title: "You Don't Know JS", price: "55"}
// } 

console.log(b);
// {
// 	name: "muyiy",
// 	book: {title: "You Don't Know JS", price: "45"}
// } 

完全改變變量 a 之後對 b 沒有任何影響,這就是深拷貝的魔力。

我們看下對數組深拷貝效果如何。

// 木易楊
let a = [0, "1", [2, 3]];
let b = JSON.parse(JSON.stringify( a.slice(1) ));
console.log(b);
// ["1", [2, 3]]

a[1] = "99";
a[2][0] = 4;
console.log(a);
// [0, "99", [4, 3]]

console.log(b);
//  ["1", [2, 3]]

對數組深拷貝之後,改變原數組不會影響到拷貝之後的數組。

但是該方法有以下幾個問題。

  1. 會忽略 undefined

  2. 會忽略 symbol

  3. 不能序列化函數

  4. 不能解決循環引用的對象

  5. 不能正確處理new Date()

  6. 不能處理正則

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

new Date 情況下,轉換結果不正確

// 木易楊
new Date();
// Mon Dec 24 2018 10:59:14 GMT+0800 (China Standard Time)

JSON.stringify(new Date());
// ""2018-12-24T02:59:25.776Z""

JSON.parse(JSON.stringify(new Date()));
// "2018-12-24T02:59:41.523Z"

解決方法轉成字符串或者時間戳就好了。

// 木易楊
let date = (new Date()).valueOf();
// 1545620645915

JSON.stringify(date);
// "1545620673267"

JSON.parse(JSON.stringify(date));
// 1545620658688

正則情況下

// 木易楊
let obj = {
    name: "muyiy",
    a: /'123'/
}
console.log(obj);
// {name: "muyiy", a: /'123'/}

let b = JSON.parse(JSON.stringify(obj));
console.log(b);
// {name: "muyiy", a: {}}

PS:爲什麼會存在這些問題可以學習一下 JSON。

除了上面介紹的深拷貝方法,常用的還有jQuery.extend() 和 lodash.cloneDeep(),後面文章會詳細介紹源碼實現,敬請期待!

總結:

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章