摘要:從而也引出了所謂的深淺復制問題。附注對于淺復制,其實還有其他的實現方式,比如數組中和方法,對于這些還是希望大家自己了解,本本主要針對深淺復制的實現原理進行解析。
前言
在之前寫繼承的過程談到了深淺復制的問題,因為有讀者反映到需要解析,趁今天周末寫一篇解析,今天的主體相對之前來說理解難度低一些,篇幅可能也比較短,諸君按需閱讀即可。
從兩種數據類型說起在js中,變量的類型可以大致分成兩種:基本數據類型和引用數據類型,其中基本數據類型指的是簡單的數據段,包括:
Undefined
Null
Boolean
Number
String(字符串在一些其他語言中是被當做對象使用的,屬于引用類型,但在js里是基本類型)
而引用類型的值指的是可能包含多個值的對象。可能上面這種描述大家都看過不少,但是有沒有思考過為什么要把數據類型這樣分呢?本質上,是因為基本數據類型保存在棧內存,而引用類型保存在堆內存中。那再進一步問:為什么要分兩種保存方式呢? 根本原因在于保存在棧內存的必須是大小固定的數據,引用類型的大小不固定,只能保存在堆內存中,但是我們可以把它的地址寫在占內存中以供我們訪問。舉個例子:
var a = 1;//定義了一個number類型 var obj1 = {//定義了一個objr類型 name:"obj" };
在執行這段代碼后,內存空間里是這樣的:
因為這種保存方式的存在,所以我們在操作變量的時候,如果是基本數據類型,則按值訪問,操作的就是變量保存的值;如果是引用類型的值,我們只是通過保存在變量中的引用類型的地址類操作實際對象。從而也引出了所謂的深淺復制問題。
緊接著上文的內容,假設有以下代碼:
//例子1 var a = 1; var b = a;//復制 console.log(b)//1 a = 2;//改變a的值 console.log(b)//1
可以看到,我們復制完b以后,即使改變a的值,b也不會改變,因為a和b是相互獨立的,按照上面的圖,也就是在棧內存中創建了一個變量b 保存的值也是2;
//例子2 var color1 = ["red","green"]; var color2 = color1;//復制 console.log(color2)//["red","green"]; color1.push("black") ;//改變color1的值 console.log(color2)//["red","green","black"]
在例子2中,我們按照完全相同的步驟,操作了一個數組,但是返回的結果卻完全不一樣,因為此時的復制,實際上是這樣:
我們只是復制了一次引用類型的地址而已,所以,不管接下來我們是操作color1還是color2,本質上都是操作同一個數組對象。
剛剛說到,簡單的賦值沒有辦法復制引用類型,那如果我們就是想復制上面的color1數組怎么辦呢?可以這樣:
var color1 = ["red","green"]; var color2 = []; //復制 for(var i = 0;i < color1.length;i++){ color2[i] = color1[i]; } console.log(color2)//["red","green"]; color1.push("black") ;//改變color1的值 console.log(color2)//["red","green"]
這一次我們先創建了一個空數組color2,然后讓color2的每個值都和color1對應相等,最后的color1和color2是相互獨立的了,滿足了我們的需要。當然對于對象類型也是一樣的,使用for-in遍歷取代這里的for循環即可。
問題真的就這樣解決了嗎?當然沒有,不過以上這種只復制了第一層屬性的方式就叫做淺復制,淺復制有什么缺陷呢?我們可以先思考一下,從直接使用=符號賦值進行復制到淺復制,能夠復制成功(成功是指復制的結果與復制源完全獨立),是因為我們復制的對象都是基本類型,怎么解釋呢?
在復制基本數據類型時,我們直接使用=完成復制
在引用類型的時候,我們循環遍歷對象,對每個屬性或值使用=完成復制
有沒有注意到上文的color1例子使用淺復制之所以能夠復制成功,是因為數組中的每一項都是基本數據類型(string),所以猜出了淺復制的局限了嗎?假如數組中某一項保存的是一個對象,或者是一個數組,又或者對象的某個屬性還是一個對象呢?(換句話說就是引用類型的某個屬性還是引用類型),如:
var person = { name:"lin", score:{ physics:85, math:99 } }
這個對象的分數score屬性就還是一個對象,那我們使用前面提到for-in遍歷復制的時候,對score的復制,不就又變成了我們前面提到的只復制了地址的情況嗎?再想想淺復制實現的原理,相信大家猜到了深復制實現的方式:對屬性中所有引用類型的值,遍歷到是基本類型的值為止,從這種方式上,我們很容易就可以想到利用遞歸來實現深復制。
function deepCopy (obj) { var result; //引用類型分數組和對象分別遞歸 if (Object.prototype.toString.call(obj) == "[object Array]") { result = [] for (i = 0; i < obj.length; i++) { result[i] = deepCopy(obj[i]) } } else if (Object.prototype.toString.call(obj) == "[object Object]") { result = {} for (var attr in obj) { result[attr] = deepCopy(obj[attr]) } } //值類型直接返回 else { return obj } return result }
上面的函數很簡單:對于傳入的參數,首先判斷是否為引用類型,如果不是,直接返回即可;如果是,循環遍歷該對象的屬性,如果某個屬性還是引用類型,則針對該屬性再次調用deepCopy函數,從而完成深復制。
附注對于淺復制,其實還有其他的實現方式,比如數組中concat和slice方法,對于這些還是希望大家自己了解,本本主要針對深淺復制的實現原理進行解析。
小結對于深淺復制的區別,其實核心的關鍵點就是是只復制了第一屬性還是完全復制了所有的屬性,可能有些地方寫的稍顯啰嗦或者描述不當,歡迎提出意見和建議(然而我并不一定會聽,哈哈)。如果對讀者有幫助,還是希望能點個推薦。以上內容屬于個人見解,如果有不同意見,歡迎指出和探討。請尊重作者的版權,轉載請注明出處,如作商用,請與作者聯系,感謝!
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/82197.html
摘要:二這么分的好處就是在于節省內存資源,便于合理回收內存詳解中的深淺復制有了上面的鋪墊,那么我們理解起深淺復制就變得容易的許多。 前言 對于前端開發來說,我們經常能夠遇到的問題就是js的深淺復制問題,通常情況下我們解決這個問題的方法就是用JSON.parse(JSON.Stringify(xx))轉換或者用類似于Inmmutable這種第三方庫來進行深復制,但是我們還是要弄懂其中原理,這樣...
摘要:中有三種數據結構棧堆隊列。前端進擊的巨人一執行上下文與執行棧,變量對象中解釋執行棧時,舉了一個乒乓球盒子的例子,來演示棧的存取方式,這里再舉個栗子搭積木。對于基本類型,棧中存儲的就是它自身的值,所以新內存空間存儲的也是一個值。 面試經常遇到的深淺拷貝,事件輪詢,函數調用棧,閉包等容易出錯的題目,究其原因,都是跟JavaScript基礎知識不牢固有關,下層地基沒打好,上層就是豆腐渣工程,...
摘要:深拷貝相比于淺拷貝速度較慢并且花銷較大。所以在賦值完成后,在棧內存就有兩個指針指向堆內存同一個數據。結果如下擴展運算符只能對一層進行深拷貝如果拷貝的層數超過了一層的話,那么就會進行淺拷貝那么我們可以看到和展開原算符對于深淺拷貝的結果是一樣。 JS中數據類型 基本數據類型: undefined、null、Boolean、Number、String和Symbol(ES6) 引用數據類型:...
閱讀 1383·2023-04-25 16:45
閱讀 1923·2021-11-17 09:33
閱讀 2312·2021-09-27 14:04
閱讀 919·2019-08-30 15:44
閱讀 2638·2019-08-30 14:24
閱讀 3420·2019-08-30 13:59
閱讀 1695·2019-08-29 17:00
閱讀 894·2019-08-29 15:33