JavaScript中物件的淺複製和深複製 原 薦
在JavaScript中,如果要複製一個變數我們應該怎麼做呢?下面這種做法是最簡單的一種形式:
//把a複製給b let a = 12; let b = a;
這種複製方法只能適用於基本型別,如果 a
是物件怎麼辦呢?我們先來看看上面的程式碼在記憶體中做了什麼事:
聲明瞭變數 a = 12
,棧記憶體會分配一塊區域來儲存,如上圖所示。把 a
賦給 b
,會在棧中重新開闢一塊區域來儲存 b
,並且 b
的值就是 a
的值。
假如 a
是物件,記憶體做了什麼事呢?來看下面的例子:
let a = {}; let b = a;
如圖所示,物件是儲存在堆記憶體中的,棧中儲存的是地址值,使用這種方式複製物件只不過是複製了該物件的引用而已,物件實體還是隻有一個。那麼物件應該怎樣複製呢?物件的複製其實也就是產生一個一模一樣的物件,物件包含屬性和方法,我們建立一個新物件,將舊物件的屬性和方法賦給新物件,這樣不就是複製了一個物件嗎?順著這個思路,我們來看下面的程式碼:
function copy(obj){ //基本型別和函式直接返回 if(!(obj instanceof Object) || typeof obj === 'function') return obj; let newObj = {}; if(obj instanceof Array) newObj = []; for(let p in obj){ newObj[p] = obj[p]; } return newObj; } let p = { name: 'bob', friends: ['jack', 'rose'] } let p2 = copy(p); console.log(p2);
定義一個 copy
函式,接收一個引數,用以實現物件的複製,如果引數是基本型別或函式就直接返回。函式體內宣告一個新物件 newObj
,然後遍歷引數 obj
,將其每一個屬性都賦給 newObj
,最後返回 newObj
。接著使用 copy
方法生成了 p
的一個複製物件 p2
,結果如下圖所示:
但是這樣有一個問題,我們來看下面的程式碼:
p2.friends.push('lily'); console.log(p2.friends);//["jack", "rose", "lily"] console.log(p.friends);//["jack", "rose", "lily"]
我們給 p2
的 friends
添加了一個 lily
,結果致使 p
的 friends
也被同步修改了。下面的記憶體圖可以幫助讀者理解:
從圖中可以看出,雖然 p
和 p2
分別指向了兩個物件,但是裡面的 friends
屬性還是指向的同一個陣列,問題在於這段程式碼:
for(let p in obj){ newObj[p] = obj[p]; }
只進行了物件第一層的複製,對於物件裡面引用型別的屬性,則進行了地址值的複製,這就是所謂的 淺複製 ,也就是說 p.friends
和 p2.friends
是同一個物件:
console.log(p.friends == p2.friends);//true
那如何進行徹底的複製—— 深複製 呢?思路也很簡單,在遍歷賦值物件屬性的時候,遇到屬性是引用型別的,也需要把這個屬性展開賦值一下,於是我們可以用遞迴的思想來實現,如下程式碼所示:
//深複製 function deepCopy(obj){ //基本型別和函式直接返回 if(!(obj instanceof Object) || typeof obj === 'function') return obj; let newObj = {}; if(obj instanceof Array) newObj = []; for(let p in obj){ if((p instanceof Object) || typeof p === 'function'){ //基本資料型別和函式 newObj[p] = obj[p]; }else{ //繼續複製物件裡面的物件 newObj[p] = deepCopy(obj[p]); } } return newObj; } let p = { name: 'bob', friends: ['jack', 'rose'] } let p2 = deepCopy(p); p2.friends.push('lily'); console.log(p2.friends);//["jack", "rose", "lily"] console.log(p.friends);//["jack", "rose"] console.log(p.friends == p2.friends);//false
聲明瞭 deepCopy
函式,用於實現深複製,函式體和淺複製的函式體基本相同,只是在遍歷要複製的物件的時候添加了一個判斷,如果屬性是基本型別或函式則進行賦值操作,否則遞迴呼叫 deepCopy
,繼續複製物件裡面的物件。接著演示了 deepCopy
的使用,發現修改了 p2.friends
並不會影響 p.friends
,它們已經是兩個物件了。
對於淺複製,ES6中提供了 Object.assign()
方法,如下程式碼所示:
let p3 = Object.assign({}, p); console.log(p3.friends == p.friends);//true
不知讀者是否還記得,在 JavaScript繼承(四)——原型式繼承 中提到過 Object.create()
方法,從使用效果上來看, Object.create()
建立的新物件有點類似淺複製,只不過新物件和原物件是一種繼承關係,而 Object.assign()
建立的新物件和原物件是彼此獨立的,如下程式碼所示:
let p4 = Object.create(p); console.log(p4.__proto__ === p);//true console.log(p3.__proto__ === p);//false
這點是讀者需要注意的。