1. 程式人生 > >一文帶你瞭解js資料儲存及深複製(深拷貝)與淺複製(淺拷貝)

一文帶你瞭解js資料儲存及深複製(深拷貝)與淺複製(淺拷貝)

## 背景 在日常開發中,偶爾會遇到需要複製物件的情況,需要進行物件的複製。 由於現在流行標題黨,所以,一文帶你瞭解js資料儲存及深複製(深拷貝)與淺複製(淺拷貝) ## 理解 首先就需要理解 js 中的資料型別了 js 資料型別包含 1. `基礎型別`:`String`、`Number`、 `null`、`undefined`、`Boolean`以及`ES6`引入的`Symbol`、`es10`中的`BigInt` 2. `引用型別`:`Object` 由於 js 對變數的儲存是`棧記憶體`、`堆記憶體`完成的。 - `基礎型別`將資料儲存在`棧記憶體`中 - `引用型別`將資料儲存在`堆記憶體`中 由於 js 在資料讀取和寫入的時候,對`基礎型別`是直接讀寫`棧記憶體`中的資料,`引用型別`是將一個記憶體地址儲存在棧記憶體中,讀寫都是修改棧記憶體中,指向堆記憶體的地址 以如下程式碼為例 ``` let obj = { a:1, arr:[1,3,5,7,9], b:2, c:{ num:100 } } let num = 10 ``` 在記憶體中的表現為 ![記憶體中的展示](https://imgkr.cn-bj.ufileos.com/08889b21-612d-44c1-916a-ff97e0b3adc2.jpg) 我們宣告個obj1 ``` let obj1 = obj; console.log(obj1 == obj);//true ``` 因為這個賦值,把記憶體變成了這樣 ![賦值後](https://imgkr.cn-bj.ufileos.com/cb8c3a11-9194-477f-94af-d8627679343a.jpg) 然後,記憶體中只是給js棧記憶體新增了一個指向`堆記憶體`的地址而已,這種就叫做`淺複製`。因為如圖可以看到,如果我們修改`obj.a`的話,實際修改的是`堆記憶體0x88888888`中的`變數a`,由於`obj1`也指向這個地址,所以`obj1.a`也被修改了 `深複製`是指,不單單複製引用地址,連堆記憶體都複製一遍,使`obj`和`obj1`不指向同一個地址。 ## 程式碼 分開來看`深複製`與`淺複製` ### 淺複製 由上述圖可知,淺複製只是複製第一層,也就是,`基本型別`複製新值,`引用型別`複製引用地址 淺複製可以使用的方案有`迴圈賦值`、`擴充套件運算子`、`object.assign()`, ``` let obj = { a:1, arr:[1,3,5,7,9], b:2, c:{ num:100 } } function clone1(obj){ // 使用迴圈賦值 let b = {}; for(let key in obj){ b[key] = obj[key] } return b } function clone2(obj){ // 使用擴充套件運算子 let b = { ...obj }; return b } function clone3(obj){ // 使用object.assign() let b = {}; Object.assign(b,obj) return b } let obj1 = clone1(obj); let obj2 = clone2(obj); let obj3 = clone3(obj); console.log(obj1 === obj); //false 代表複製成功了 console.log(obj2 === obj); //false 代表複製成功了 console.log(obj3 === obj); //false 代表複製成功了 console.log('obj0.c.num修改前',obj.c.num); //100 console.log('obj1.c.num修改前',obj1.c.num); //100 console.log('obj2.c.num修改前',obj2.c.num); //100 console.log('obj3.c.num修改前',obj3.c.num); //100 obj0.c.num = 555; console.log('obj0.c.num修改後',obj.c.num); //555 console.log('obj1.c.num修改後',obj1.c.num); //555 console.log('obj2.c.num修改後',obj2.c.num); //555 console.log('obj3.c.num修改後',obj3.c.num); //555 ``` 由於是淺複製,所以引用型別只是複製了記憶體地址,修改其中一個物件的子屬性後,引用這個地址的值都會被修改。 ### 深複製 由於淺複製只是複製第一層,為了解決引用型別的複製,需要使用深複製來完成物件的複製,`基本型別`複製新值,`引用型別`開闢新的`堆記憶體`。 淺複製可以使用的方案有`JSON.parse(JSON.stringify(obj))`、`迴圈賦值`。 #### JSON.parse(JSON.stringify(obj)) ``` let obj = { a:1, arr:[1,3,5,7,9], c:{ num:100 }, fn:function(){ console.log(1) }, date:new Date(), reg:/\.*/g } function clone1(obj){ // 使用JSON.parse(JSON.stringify(obj)) return JSON.parse(JSON.stringify(obj)) } let obj1 = clone1(obj); console.log(obj === obj1); //false 代表複製成功了 obj.c.num = 555; console.log(obj.c.num,obj1.c.num) // 555,100 ``` 看起來是複製成功了!!~地址也變了,修改`obj`,`obj1`的引用地址不會跟著變化。 但是我們來`console`一下`obj`以及`obj1` ``` console.log(obj) console.log(obj1) ``` ![列印結果](https://imgkr.cn-bj.ufileos.com/5e000cff-e658-4162-a6d5-246ddb44d9a9.png) 似乎發現了離奇的事情,只有`obj.a`以及`obj.c`正確的複製了,`日期型別`、`方法`、`正則表示式`均沒有複製成功,發生了一些奇怪的事情 #### 迴圈賦值 deepClone 那麼為了解決這種事情,就需要寫一個`deepClone`方法來完成深複製了,參考了許多開源庫的寫法,將所有的複製項單獨拆出,方便未來對特殊型別進行擴充套件,也防止不同功能間的變數互相干擾 ``` //既然是深複製,一定要傳入一個object,再return 一個新的 Object function deepClone(obj){ let newObj; if(obj instanceof Array){ // 陣列的話,要new一個數組 newObj = [] }else if(obj instanceof Object){ // 物件的話,要new一個物件 newObj = {} } if(obj === null) { return cloneNull(obj) } if(typeof obj=='function'){ return cloneFunction(obj) } if(typeof obj!='object') { return cloneOther(obj) } if(obj instanceof RegExp) { return cloneRegExp(obj) } if(obj instanceof Date){ return cloneDate(obj) } if(obj instanceof Array){ for(let index in obj){ newObj[index] = deepClone(obj[index]); // 對陣列子項進行復制 } } if(obj instanceof Object){ for(let key in obj){ newObj[key] = deepClone(obj[key]); // 對物件子項進行復制 } } return newObj; } function cloneNull(obj){ // 複製NULL return obj } function cloneFunction(obj){ // 複製方法, //這個方法待完善,暫時未找到能夠完美複製function的方案,如果有方案,望指出 return obj } function cloneOther(obj){ // 複製非物件的資料 return obj } function cloneRegExp(obj){ // 複製正則物件 return new RegExp(obj) } function cloneDate(obj){ // 複製日期物件 return new Date(obj) } ``` 這樣一個基本上滿足功能的深複製就完成了。先測試一下 ``` let obj = { a:1, arr:[1,3,5,7,9], c:{ num:100 }, fn:function(){ console.log(1) }, date:new Date(), reg:/\.*/g } let obj1 = deepClone(obj); console.log(obj.c === obj1.c); // false 代表複製成功 console.log(obj.fn === obj1.fn);// true 由於方法單純修改了引用的地址,所以這裡是淺複製 console.log(obj.date === obj1.date);// false 代表複製成功 console.log(obj.reg === obj1.reg);// false 代表複製成功 ``` 再`console`一下 ``` console.log(obj) console.log(obj1) ``` ![列印結果](https://imgkr.cn-bj.ufileos.com/e74e8f1e-8a74-4990-a7f5-dac0681ca085.png) 這樣,就完成了`deepClone`深複製方法 經過深複製後,圖解如下 ![深複製後記憶體圖解](https://imgkr.cn-bj.ufileos.com/4c9caffb-6cd8-4e89-b031-617f67097c66.jpg) #### 優化 deepClone 上述程式碼還有優化空間,參考了`lodash`庫,在進行 new 物件時,可以使用 `constructor`建構函式 來進行建立新的例項,這樣 1. 可以不用判斷遞迴中,是陣列還是物件 2. 如果深複製的某一項是某個原型的例項,深複製完成後,依然是該原型的例項 ``` function deepClone(obj){ let newObj = new obj.constructor; if(obj === null) { return cloneNull(obj) } if(typeof obj=='function'){ return cloneFunction(obj) } if(typeof obj!='object') { return cloneOther(obj) } if(obj instanceof RegExp) { return cloneRegExp(obj) } if(obj instanceof Date){ return cloneDate(obj) } if(obj instanceof Array){ for(let index in obj){ newObj[index] = deepClone(obj[index]); // 對陣列子項進行復制 } } if(obj instanceof Object){ for(let key in obj){ newObj[key] = deepClone(obj[key]); // 對物件子項進行復制 } } return newObj; } function cloneNull(obj){ // 複製NULL return obj } function cloneFunction(obj){ // 複製方法, //這個方法待完善,暫時未找到能夠完美複製function的方案,如果有方案,望指出 return obj } function cloneOther(obj){ // 複製非物件的資料 return obj } function cloneRegExp(obj){ // 複製正則物件 return new RegExp(obj) } function cloneDate(obj){ // 複製日期物件 return new Date(obj) } ``` #### 最終版本 deepClone 然後可以有一個合併版本的,比較節省程式碼,將下方區分開的複製方法,合併到`deepClone`中,可以極大地減少程式碼體積 ``` function deepClone(obj){ // let newObj = new obj.constructor; if(obj === null) return obj // if(typeof obj=='function') return obj // 由於typeof obj=='function'也符合下方的typeof obj!='object',所以此條可以省略 if(typeof obj!='object') return obj if(obj instanceof RegExp) return new RegExp(obj) if(obj instanceof Date) return new Date(obj) // 執行到這裡,基本上只存在陣列和物件兩種型別了 for(let index in obj){ newObj[index] = deepClone(obj[index]); // 對子項進行遞迴複製 } return newObj