您好,登錄后才能下訂單哦!
本篇內容主要講解“javascript深拷貝、淺拷貝和循環引用的詳細講解”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓小編來帶大家學習“javascript深拷貝、淺拷貝和循環引用的詳細講解”吧!
一、為什么有深拷貝和淺拷貝?
這個要從js中的數據類型說起,js中數據類型分為基本數據類型和引用數據類型。
基本類型值指的是那些保存在棧內存中的簡單數據段,即這種值是完全保存在內存中的一個位置。包含Number,String,Boolean,Null,Undefined ,Symbol。
引用類型值指的是那些保存在堆內存中的對象,所以引用類型的值保存的是一個指針,這個指針指向存儲在堆中的一個對象。除了上面的 6 種基本數據類型外,剩下的就是引用類型了,統稱為 Object 類型。細分的話,有:Object 類型、Array 類型、Date 類型、RegExp 類型、Function 類型 等。
正因為引用類型的這種機制, 當我們從一個變量向另一個變量復制引用類型的值時,實際上是將這個引用類型在棧內存中的引用地址復制了一份給新的變量,其實就是一個指針。因此當操作結束后,這兩個變量實際上指向的是同一個在堆內存中的對象,改變其中任意一個對象,另一個對象也會跟著改變。
因此深拷貝和淺拷貝只發生在引用類型中。簡單來說他們的區別在于:
1. 層次
淺拷貝 只會將對象的各個屬性進行依次復制,并不會進行遞歸復制,也就是說只會賦值目標對象的第一層屬性。
深拷貝不同于淺拷貝,它不只拷貝目標對象的第一層屬性,而是遞歸拷貝目標對象的所有屬性。
2. 是否開辟新的棧
淺拷貝 對于目標對象第一層為基本數據類型的數據,就是直接賦值,即「傳值」;而對于目標對象第一層為引用數據類型的數據,就是直接賦存于棧內存中的堆內存地址,即「傳址」,并沒有開辟新的棧,也就是復制的結果是兩個對象指向同一個地址,修改其中一個對象的屬性,則另一個對象的屬性也會改變,
深拷貝 而深復制則是開辟新的棧,兩個對象對應兩個不同的地址,修改一個對象的屬性,不會改變另一個對象的屬性。
二、淺拷貝
以下是實現淺拷貝的幾種實現方式:
1.Array.concat()
const arr = [1,2,3,4,[5,6]]; const copy = arr.concat(); \\ 利用concat()創建arr的副本 \\改變基本類型值,不會改變原數組 copy[0] = 2; arr; //[1,2,3,4,[5,6]]; \\改變數組中的引用類型值,原數組也會跟著改變 copy[4][1] = 7; arr; //[1,2,3,4,[5,7]];
能實現類似效果的還有slice()和Array.from()等,大家可以自己嘗試一下~
2.Object.assign()
const obj1 = {x: 1, y: 2}; const obj2 = Object.assign({}, obj1); obj2.x = 2; \\修改obj2.x,改變對象中的基本類型值 console.log(obj1) //{x: 1, y: 2} //原對象未改變 console.log(obj2) //{x: 2, y: 2}
const obj1 = { x: 1, y: { m: 1 } }; const obj2 = Object.assign({}, obj1); obj2.y.m = 2; \\修改obj2.y.m,改變對象中的引用類型值 console.log(obj1) //{x: 1, y: {m: 2}} 原對象也被改變 console.log(obj2) //{x: 2, y: {m: 2}}
三、深拷貝
1.JSON.parse()和JSON.stringify()
const obj1 = { x: 1, y: { m: 1 } }; const obj2 = JSON.parse(JSON.stringify(obj1)); console.log(obj1) //{x: 1, y: {m: 1}} console.log(obj2) //{x: 1, y: {m: 1}} obj2.y.m = 2; //修改obj2.y.m console.log(obj1) //{x: 1, y: {m: 1}} 原對象未改變 console.log(obj2) //{x: 2, y: {m: 2}}
這種方法使用較為簡單,可以滿足基本日常的深拷貝需求,而且能夠處理JSON格式能表示的所有數據類型,但是有以下幾個缺點:
undefined、任意的函數、正則表達式類型以及 symbol 值,在序列化過程中會被忽略(出現在非數組對象的屬性值中時)或者被轉換成 null(出現在數組中時);
它會拋棄對象的constructor。也就是深拷貝之后,不管這個對象原來的構造函數是什么,在深拷貝之后都會變成Object;
如果對象中存在循環引用的情況無法正確處理。
2.遞歸
function deepCopy1(obj) { // 創建一個新對象 let result = {} let keys = Object.keys(obj), key = null, temp = null; for (let i = 0; i < keys.length; i++) { key = keys[i]; temp = obj[key]; // 如果字段的值也是一個對象則遞歸操作 if (temp && typeof temp === 'object') { result[key] = deepCopy(temp); } else { // 否則直接賦值給新對象 result[key] = temp; } } return result; } const obj1 = { x: { m: 1 }, y: undefined, z: function add(z1, z2) { return z1 + z2 }, a: Symbol("foo") }; const obj2 = deepCopy1(obj1); obj2.x.m = 2; console.log(obj1); //{x: {m: 1}, y: undefined, z: ?, a: Symbol(foo)} console.log(obj2); //{x: {m: 2}, y: undefined, z: ?, a: Symbol(foo)}
四、循環引用
看似遞歸已經完全解決我們的問題了,然而還有一種情況我們沒考慮到,那就是循環引用
1.父級引用
這里的父級引用指的是,當對象的某個屬性,正是這個對象本身,此時我們如果進行深拷貝,可能會在子元素->父對象->子元素...這個循環中一直進行,導致棧溢出。比如下面這個例子:
const obj1 = { x: 1, y: 2 }; obj1.z = obj1; const obj2 = deepCopy1(obj1); \\棧溢出
解決辦法是:只需要判斷一個對象的字段是否引用了這個對象或這個對象的任意父級即可,可以修改上面的deepCopy函數:
function deepCopy2(obj, parent=null) { //創建一個新對象 let result = {}; let keys = Object.keys(obj), key = null, temp = null, _parent = parent; //該字段有父級則需要追溯該字段的父級 while(_parent) { //如果該字段引用了它的父級,則為循環引用 if(_parent.originParent === obj) { //循環引用返回同級的新對象 return _parent.currentParent; } _parent = _parent.parent } for(let i=0,len=keys.length;i<len;i++) { key = keys[i] temp = obj[key] // 如果字段的值也是一個新對象 if(temp && typeof temp === 'object') { result[key] = deepCopy(temp, { //遞歸執行深拷貝,將同級的待拷貝對象與新對象傳遞給parent,方便追溯循環引用 originParent: obj, currentParent: result, parent: parent }); } else { result[key] = temp; } } return result; } const obj1 = { x:1 } obj1.z = obj1; const obj2 = deepCopy2(obj1);
2. 同級引用
假設對象obj有a,b,c三個子對象,其中子對象c中有個屬性d引用了對象obj下面的子對象a。
const obj= { a: { name: 'a' }, b: { name: 'b' }, c: { } }; c.d.e = obj.a;
此時c.d.e和obj.a 是相等的,因為它們引用的是同一個對象
console.log(c.d.e === obj.a); //true
如果我們調用上面的deepCopy2函數
const copy = deepCopy2(obj); console.log(copy.a); // 輸出: {name: "a"} console.log(copy.d.e);// 輸出: {name: "a"} console.log(copy.a === copy.d.e); // 輸出: false
以上表現我們就可以看出,雖然opy.a 和copy.d.e在字面意義上是相等的,但二者并不是引用的同一個對象,這點上來看對象copy和原對象obj還是有差異的。
這種情況是因為obj.a并不在obj.d.e的父級對象鏈上,所以deepCopy2函數就無法檢測到obj.d.e對obj.a也是一種引用關系,所以deepCopy2函數就將obj.a深拷貝的結果賦值給了copy.d.e。
解決方案:父級的引用是一種引用,非父級的引用也是一種引用,那么只要記錄下對象A中的所有對象,并與新創建的對象一一對應即可。
function deepCopy3(obj) { // hash表,記錄所有的對象的引用關系 let map = new WeakMap(); function dp(obj) { let result = null; let keys = Object.keys(obj); let key = null, temp = null, existobj = null; existobj = map.get(obj); //如果這個對象已經被記錄則直接返回 if(existobj) { return existobj; } result = {} map.set(obj, result); for(let i =0,len=keys.length;i<len;i++) { key = keys[i]; temp = obj[key]; if(temp && typeof temp === 'object') { result[key] = dp(temp); }else { result[key] = temp; } } return result; } return dp(obj); } const obj= { a: { name: 'a' }, b: { name: 'b' }, c: { } }; c.d.e = obj.a; const copy = deepCopy3(obj);
到此,相信大家對“javascript深拷貝、淺拷貝和循環引用的詳細講解”有了更深的了解,不妨來實際操作一番吧!這里是億速云網站,更多相關內容可以進入相關頻道進行查詢,關注我們,繼續學習!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。