js中的深拷貝和淺拷貝


深拷貝和淺拷貝的本質區別就是深拷貝是新開辟一個新的地址空間,對象的改變不會影響原數組;淺拷貝只是復制原對象,指針仍然指向原數組,當前數組變化的時候會觸發原數組的改變。

寫在前面

各類技術論壇關於深拷貝的博客有很多,有些寫的也比我好,那為什么我還要堅持寫這篇博客呢,之前看到的一篇博客中有句話寫的非常好

學習就好比是座大山,人們沿着不同的路登山,分享着自己看到的風景。你不一定能看到別人看到的風景,體會到別人的心情。只有自己去登山,才能看到不一樣的風景,體會才更加深刻。

寫博客的初衷也是作為自己學到的知識點的總結,同時也希望能給點開這篇文章的人一些幫助,在前端開發的路上能夠少一點坎坷多一點希望

如有錯誤歡迎指出會在第一時間改正

基本類型的值和引用類型的值

JavaScript的變量中包含兩種類型的值

  1. 基本類型值
    基本類型值指的是存儲在棧中的一些簡單的數據段
var str = 'a'; var num = 1; 

在JavaScript中基本數據類型有String,Number,Undefined,Null,Boolean,在ES6中,又定義了一種新的基本數據類型Symbol,所以一共有6種

基本類型是按值訪問的,從一個變量復制基本類型的值到另一個變量后這2個變量的值是完全獨立的,即使一個變量改變了也不會影響到第二個變量

var str1 = 'a'; var str2 = str1; str2 = 'b'; console.log(str2); //'b' console.log(str1); //'a' 
  1. 引用類型值
    引用類型值是引用類型的實例,它是保存在堆內存中的一個對象,引用類型是一種數據結構,最常用的是Object,Array,Function類型,另外還有Date,RegExp,Error等,ES6同樣也提供了Set,Map2種新的數據結構

JavaScript是如何復制引用類型的

JavaScript對於基本類型和引用類型的賦值是不一樣的

var obj1 = {a:1}; var ob2 = obj1; obj2.a = 2; console.log(obj1); //{a:2} console.log(obj2); //{a:2} 

在這里只修改了obj1中的a屬性,卻同時改變了ob1和obj2中的a屬性

當變量復制引用類型值的時候,同樣和基本類型值一樣會將變量的值復制到新變量上,不同的是對於變量的值,它是一個指針,指向存儲在堆內存中的對象(JS規定放在堆內存中的對象無法直接訪問,必須要訪問這個對象在堆內存中的地址,然后再按照這個地址去獲得這個對象中的值,所以引用類型的值是按引用訪問)

變量的值也就是這個指針是存儲在棧上的,當變量obj1復制變量的值給變量obj2時,obj1,obj2只是一個保存在棧中的指針,指向同一個存儲在堆內存中的對象,所以當通過變量obj1操作堆內存的對象時,obj2也會一起改變


 
保存在於棧中的變量和堆內存中對象的關系

再舉個例子,小明(obj1變量)知道他家的地址(對象{a:1}),然后小明告訴了小剛(obj2變量)他家的地址(復制變量),小剛這個時候就知道了小明家的地址,然后小剛去小明家把小明家的門給拆了(修改對象),小明回家一看就會發現門沒了,這時小明和小剛去這個地址的時候都會看到一個沒有門的家-.-(對象的修改反映到變量)

淺拷貝

對於淺拷貝的定義可以理解為

創建一個新對象,這個對象有着原始對象屬性值的一份精確拷貝。如果屬性是基本類型,拷貝的就是基本類型的值,如果屬性是引用類型,拷貝的就是內存地址 ,所以如果其中一個對象改變了這個地址,就會影響到另一個對象。

以下是一些JavaScript提供的淺拷貝方法

Object.assign()

ES6中拷貝對象的方法,接受的第一個參數是拷貝的目標,剩下的參數是拷貝的源對象(可以是多個)

語法:Object.assign(target, ...sources)

var target = {}; var source = {a:1}; Object.assign(target ,source); console.log(target); //{a:1} source.a = 2; console.log(source); //{a:2} console.log(target); //{a:1} 

Object.assign是一個淺拷貝,它只是在根屬性(對象的第一層級)創建了一個新的對象,但是對於屬性的值是仍是對象的話依然是淺拷貝,

Object.assign還有一些注意的點是:

  1. 不會拷貝對象繼承的屬性
  2. 不可枚舉的屬性
  3. 屬性的數據屬性/訪問器屬性
  4. 可以拷貝Symbol類型

可以理解為Object.assign就是使用簡單的=來賦值,遍歷從右往左遍歷源對象(sources)的所有屬性用 = 賦值到目標對象(target)上

var obj1 = { a:{ b:1 }, sym:Symbol(1) }; Object.defineProperty(obj1,'innumerable',{ value:'不可枚舉屬性', enumerable:false }); var obj2 = {}; Object.assign(obj2,obj1) obj1.a.b = 2; console.log('obj1',obj1); console.log('obj2',obj2); 
 
image

可以看到Symbol類型可以正確拷貝,但是不可枚舉的屬性被忽略了並且改變了obj1.a.b的值,obj2.a.b的值也會跟着改變,說明依舊存在訪問的是堆內存中同一個對象的問題

擴展運算符

利用擴展運算符可以在構造字面量對象時,進行克隆或者屬性拷貝

語法:var cloneObj = { ...obj };

var obj = {a:1,b:{c:1}} var obj2 = {...obj}; obj.a=2; console.log(obj); //{a:2,b:{c:1}} console.log(obj2); //{a:1,b:{c:1}} obj.b.c = 2; console.log(obj); //{a:2,b:{c:2}} console.log(obj2); //{a:1,b:{c:2}} 

擴展運算符Object.assign()有同樣的缺陷,對於值是對象的屬性無法完全拷貝成2個不同對象,但是如果屬性都是基本類型的值的話,使用擴展運算符更加方便

Array.prototype.slice()

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

語法: arr.slice(begin, end);

在ES6以前,沒有剩余運算符,Array.from的時候可以用 Array.prototype.slice將arguments類數組轉為真正的數組,它返回一個淺拷貝后的的新數組

Array.prototype.slice.call({0: "aaa", length: 1}) //["aaa"] let arr = [1,2,3,4] console.log(arr.slice() === arr); //false 

深拷貝

淺拷貝只在根屬性上在堆內存中創建了一個新的的對象,復制了基本類型的值,但是復雜數據類型也就是對象則是拷貝相同的地址,而深拷貝則是對於復雜數據類型在堆內存中開辟了一塊內存地址用於存放復制的對象並且把原有的對象復制過來,這2個對象是相互獨立的,也就是2個不同的地址

將一個對象從內存中完整的拷貝一份出來,從堆內存中開辟一個新的區域存放新對象,且修改新對象不會影響原對象

一個簡單的深拷貝

var obj1 = { a: { b: 1 }, c: 1 }; var obj2 = {}; obj2.a = {} obj2.c = obj1.c obj2.a.b = obj1.a.b; console.log(obj1); //{a:{b:1},c:1}; console.log(obj2); //{a:{b:1},c:1}; obj1.a.b = 2; console.log(obj1); //{a:{b:2},c:1}; console.log(obj2); //{a:{b:1},c:1}; 

在上面的代碼中,我們新建了一個obj2對象,同時根據obj1對象的a屬性是一個引用類型,我們給obj2.a的值也新建一個新對象(即在內存中新開辟了一塊內存地址),然后把obj1.a.b屬性的值數字1復制給obj2.a.b,因為數字1是基本類型的值,所以改變obj1.a.b的值后,obj2.a不會收到影響,因為他們的引用是完全2個獨立的對象,這就完成了一個簡單的深拷貝

JSON.stringify()

JSON.stringify()是目前前端開發過程中最常用的深拷貝方式,原理是把一個對象序列化成為一個JSON字符串,將對象的內容轉換成字符串的形式再保存在磁盤上,再用JSON.parse()反序列化將JSON字符串變成一個新的對象

var obj1 = { a:1, b:[1,2,3] } var str = JSON.stringify(obj1) var obj2 = JSON.parse(str) console.log(obj2); //{a:1,b:[1,2,3]} obj1.a=2 obj1.b.push(4); console.log(obj1); //{a:2,b:[1,2,3,4]} console.log(obj2); //{a:1,b:[1,2,3]} 

通過JSON.stringify實現深拷貝有幾點要注意

  1. 拷貝的對象的值中如果有函數,undefined,symbol則經過JSON.stringify()序列化后的JSON字符串中這個鍵值對會消失
  2. 無法拷貝不可枚舉的屬性,無法拷貝對象的原型鏈
  3. 拷貝Date引用類型會變成字符串
  4. 拷貝RegExp引用類型會變成空對象
  5. 對象中含有NaN、Infinity和-Infinity,則序列化的結果會變成null
  6. 無法拷貝對象的循環應用(即obj[key] = obj)
function Obj() { this.func = function () { alert(1) }; this.obj = {a:1}; this.arr = [1,2,3]; this.und = undefined; this.reg = /123/; this.date = new Date(0); this.NaN = NaN this.infinity = Infinity this.sym = Symbol(1) } var obj1 = new Obj(); Object.defineProperty(obj1,'innumerable',{ enumerable:false, value:'innumerable' }) console.log('obj1',obj1); var str = JSON.stringify(obj1); var obj2 = JSON.parse(str); console.log('obj2',obj2); 

打印出來的結果如下

 
image

可以看到除了Object對象和數組其他基本都和原來的不一樣,obj1的constructor是Obj(),而obj2的constructor指向了Object(),而對於循環引用則是直接報錯了

雖說通過JSON.stringify()方法深拷貝對象也有很多無法實現的功能,但是對於日常的開發需求(對象和數組),使用這種方法是最簡單和快捷的

使用第三方庫實現對象的深拷貝

1.lodash

2.jQuery

以上2個第三方的庫都很好的封裝的深拷貝的方法,有興趣的同學可以去深入研究一下

自己來實現一個深拷貝函數

遞歸

這里簡單封裝了一個deepClone的函數,for in遍歷傳入參數的值,如果值是引用類型則再次調用deepClone函數,並且傳入第一次調用deepClone參數的值作為第二次調用deepClone的參數,如果不是引用類型就直接復制

var obj1 = { a:{ b:1 } }; function deepClone(obj) { var cloneObj = {}; //在堆內存中新建一個對象 for(var key in obj){ //遍歷參數的鍵 if(typeof obj[key] ==='object'){ cloneObj[key] = deepClone(obj[key]) //值是對象就再次調用函數 }else{ cloneObj[key] = obj[key] //基本類型直接復制值 } } return cloneObj } var obj2 = deepClone(obj1); obj1.a.b = 2; console.log(obj2); //{a:{b:1}} 

但是還有很多問題

  • 首先這個deepClone函數並不能復制不可枚舉的屬性以及Symbol類型
  • 這里只是針對Object引用類型的值做的循環迭代,而對於Array,Date,RegExp,Error,Function引用類型無法正確拷貝
  • 對象循環引用成環了的情況

本人總結的深拷貝的方法

看過很多關於深拷貝的博客,本人總結出了一個能夠深拷貝ECMAScript的原生引用類型的方法

const isComplexDataType = obj => (typeof obj === 'object' || typeof obj === 'function') && (obj !== null) const deepClone = function (obj, hash = new WeakMap()) { if (obj.constructor === Date) return new Date(obj); //日期對象就返回一個新的日期對象 if (obj.constructor === RegExp) return new RegExp(obj); //正則對象就返回一個新的正則對象 //如果成環了,參數obj = obj.loop = 最初的obj 會在WeakMap中找到第一次放入的obj提前返回第一次放入WeakMap的cloneObj if (hash.has(obj)) return hash.get(obj) let allDesc = Object.getOwnPropertyDescriptors(obj); //遍歷傳入參數所有鍵的特性 let cloneObj = Object.create(Object.getPrototypeOf(obj), allDesc); //繼承原型鏈 hash.set(obj, cloneObj) for (let key of Reflect.ownKeys(obj)) { //Reflect.ownKeys(obj)可以拷貝不可枚舉屬性和符號類型 // 如果值是引用類型(非函數)則遞歸調用deepClone cloneObj[key] = (isComplexDataType(obj[key]) && typeof obj[key] !== 'function') ? deepClone(obj[key], hash) : obj[key]; } return cloneObj; }; let obj = { num: 0, str: '', boolean: true, unf: undefined, nul: null, obj: { name: '我是一個對象', id: 1 }, arr: [0, 1, 2], func: function () { console.log('我是一個函數') }, date: new Date(0), reg: new RegExp('/我是一個正則/ig'), [Symbol('1')]: 1, }; Object.defineProperty(obj, 'innumerable', { enumerable: false, value: '不可枚舉屬性' }); obj = Object.create(obj, Object.getOwnPropertyDescriptors(obj)) obj.loop = obj let cloneObj = deepClone(obj); console.log('obj', obj); console.log('cloneObj', cloneObj); for (let key of Object.keys(cloneObj)) { if (typeof cloneObj[key] === 'object' || typeof cloneObj[key] === 'function') { console.log(`${key}相同嗎? `, cloneObj[key] === obj[key]) } } 

這個函數有幾個要點

  1. 利用Reflect.ownKeys()方法,能夠遍歷對象的不可枚舉屬性和Symbol類型
  2. 當參數為Date,RegExp類型則直接生成一個新的實例
  3. 使用Object.getOwnPropertyDescriptors()獲得對象的所有屬性對應的特性,結合Object.create()創建一個新對象繼承傳入原對象的原型鏈
  4. 利用WeekMap()類型作為哈希表,WeekMap()因為是弱引用的可以有效的防止內存泄露,作為檢測循環引用很有幫助,如果存在循環引用直接返回WeekMap()存儲的值

這里我用全等判斷打印了2個對象的屬性是否相等,通過打印的結果可以看到,雖然值是一樣的,但是在內存中是兩個完全獨立的對象

 
image

上述的深拷貝函數中Null和Function類型引用的還是同一個對象,因為deepClone函數對於對象的值是函數或者null時直接返回,這里沒有深拷貝函數,如果需要深拷貝一個函數,可以考慮使用Function構造函數或者eval?這里還有待研究

總結

  1. 封裝的deepClone方法雖然能實現對ECMAScript原生引用類型的拷貝,但是對於對象來說范圍太廣了,仍有很多無法准確拷貝的(比如DOM節點),但是在日常開發中一般並不需要拷貝很多特殊的引用類型,深拷貝對象使用JSON.stringify依然是最方便的方法之一(當然也需要了解JSON.stringify的缺點)

  2. 實現一個完整的深拷貝是非常復雜的,需要考慮到很多邊界情況,這里我也只是對部分的原生的構造函數進行了深拷貝,對於特殊的引用類型有拷貝需求的話,建議還是借助第三方完整的庫

  3. 對於深入研究深拷貝的原理有助於理解JavaScript引用類型的特點,以及遇到相關特殊的問題也能迎刃而解,對於提高JavaScript的基礎還是很有幫助的~~~

感謝觀看

參考資料

深入JS深拷貝對象

JavaScript高級程序設計第三版



作者:心_c2a2
鏈接:https://www.jianshu.com/p/c651aeabf582
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM