您好,登錄后才能下訂單哦!
今天小編給大家分享一下JavaScript中的深拷貝如何實現的相關知識點,內容詳細,邏輯清晰,相信大部分人都還太了解這方面的知識,所以分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后有所收獲,下面我們一起來了解一下吧。
這里先直接給出最終的代碼版本,方便想快速了解的人查看,當然,你想一步步了解可以繼續查看文章余下的內容:
function deepClone(target) {
const map = new WeakMap()
function isObject(target) {
return (typeof target === 'object' && target ) || typeof target === 'function'
}
function clone(data) {
if (!isObject(data)) {
return data
}
if ([Date, RegExp].includes(data.constructor)) {
return new data.constructor(data)
}
if (typeof data === 'function') {
return new Function('return ' + data.toString())()
}
const exist = map.get(data)
if (exist) {
return exist
}
if (data instanceof Map) {
const result = new Map()
map.set(data, result)
data.forEach((val, key) => {
if (isObject(val)) {
result.set(key, clone(val))
} else {
result.set(key, val)
}
})
return result
}
if (data instanceof Set) {
const result = new Set()
map.set(data, result)
data.forEach(val => {
if (isObject(val)) {
result.add(clone(val))
} else {
result.add(val)
}
})
return result
}
const keys = Reflect.ownKeys(data)
const allDesc = Object.getOwnPropertyDescriptors(data)
const result = Object.create(Object.getPrototypeOf(data), allDesc)
map.set(data, result)
keys.forEach(key => {
const val = data[key]
if (isObject(val)) {
result[key] = clone(val)
} else {
result[key] = val
}
})
return result
}
return clone(target)
}
先看看JS數據類型圖(除了Object
,其他都是基礎類型):
在JavaScript中,基礎類型值的復制是直接拷貝一份新的一模一樣的數據,這兩份數據相互獨立,互不影響。而引用類型值(Object類型)的復制是傳遞對象的引用(也就是對象所在的內存地址,即指向對象的指針),相當于多個變量指向同一個對象,那么只要其中的一個變量對這個對象進行修改,其他的變量所指向的對象也會跟著修改(因為它們指向的是同一個對象)。如下圖:
深淺拷貝主要針對的是Object類型,基礎類型的值本身即是復制一模一樣的一份,不區分深淺拷貝。這里我們先給出測試的拷貝對象,大家可以拿這個obj
對象來測試一下自己寫的深拷貝函數是否完善:
// 測試的obj對象
const obj = {
// =========== 1.基礎數據類型 ===========
num: 0, // number
str: '', // string
bool: true, // boolean
unf: undefined, // undefined
nul: null, // null
sym: Symbol('sym'), // symbol
bign: BigInt(1n), // bigint
// =========== 2.Object類型 ===========
// 普通對象
obj: {
name: '我是一個對象',
id: 1
},
// 數組
arr: [0, 1, 2],
// 函數
func: function () {
console.log('我是一個函數')
},
// 日期
date: new Date(0),
// 正則
reg: new RegExp('/我是一個正則/ig'),
// Map
map: new Map().set('mapKey', 1),
// Set
set: new Set().add('set'),
// =========== 3.其他 ===========
[Symbol('1')]: 1 // Symbol作為key
};
// 4.添加不可枚舉屬性
Object.defineProperty(obj, 'innumerable', {
enumerable: false,
value: '不可枚舉屬性'
});
// 5.設置原型對象
Object.setPrototypeOf(obj, {
proto: 'proto'
})
// 6.設置loop成循環引用的屬性
obj.loop = obj
obj
對象在Chrome瀏覽器中的結果:
淺拷貝: 創建一個新的對象,來接受你要重新復制或引用的對象值。如果對象屬性是基本的數據類型,復制的就是基本類型的值給新對象;但如果屬性是引用數據類型,復制的就是內存中的地址,如果其中一個對象改變了這個內存中的地址所指向的對象,肯定會影響到另一個對象。
首先我們看看一些淺拷貝的方法(詳細了解可點擊對應方法的超鏈接):
方法 | 使用方式 | 注意事項 |
---|---|---|
Object.assign() | Object.assign(target, ...sources) 說明:用于將所有可枚舉屬性的值從一個或多個源對象分配到目標對象。它將返回目標對象。 | 1.不會拷貝對象的繼承屬性; 2.不會拷貝對象的不可枚舉的屬性; 3.可以拷貝 Symbol 類型的屬性。 |
展開語法 | let objClone = { ...obj }; | 缺陷和Object.assign() 差不多,但是如果屬性都是基本類型的值,使用擴展運算符進行淺拷貝會更加方便。 |
Array.prototype.concat()拷貝數組 | const new_array = old_array.concat(value1[, value2[, ...[, valueN]]]) | 淺拷貝,適用于基本類型值的數組 |
Array.prototype.slice()拷貝數組 | arr.slice([begin[, end]]) | 淺拷貝,適用于基本類型值的數組 |
這里只列舉了常用的幾種方式,除此之外當然還有其他更多的方式。注意,我們直接使用=
賦值不是淺拷貝,因為它是直接指向同一個對象了,并沒有返回一個新對象。
手動實現一個淺拷貝:
function shallowClone(target) {
if (typeof target === 'object' && target !== null) {
const cloneTarget = Array.isArray(target) ? [] : {};
for (let prop in target) {
if (target.hasOwnProperty(prop)) {
cloneTarget[prop] = target[prop];
}
}
return cloneTarget;
} else {
return target;
}
}
// 測試
const shallowCloneObj = shallowClone(obj)
shallowCloneObj === obj // false,返回的是一個新對象
shallowCloneObj.arr === obj.arr // true,對于對象類型只拷貝了引用
從上面這段代碼可以看出,利用類型判斷(查看typeof),針對引用類型的對象進行 for 循環遍歷對象屬性賦值給目標對象的屬性(for...in
語句以任意順序遍歷一個對象的除Symbol以外的可枚舉屬性,包含原型上的屬性。查看for…in),基本就可以手工實現一個淺拷貝的代碼了。
深拷貝:創建一個新的對象,將一個對象從內存中完整地拷貝出來一份給該新對象,并從堆內存中開辟一個全新的空間存放新對象,且新對象的修改并不會改變原對象,二者實現真正的分離。
看看現存的一些深拷貝的方法:
JSON.stringfy()
其實就是將一個 JavaScript 對象或值轉換為 JSON 字符串,最后再用 JSON.parse()
的方法將JSON 字符串生成一個新的對象。(點這了解:JSON.stringfy()、JSON.parse())
使用如下:
function deepClone(target) {
if (typeof target === 'object' && target !== null) {
return JSON.parse(JSON.stringify(target));
} else {
return target;
}
}
// 開頭的測試obj存在BigInt類型、循環引用,JSON.stringfy()執行會報錯,所以除去這兩個條件進行測試
const clonedObj = deepClone(obj)
// 測試
clonedObj === obj // false,返回的是一個新對象
clonedObj.arr === obj.arr // false,說明拷貝的不是引用
瀏覽器執行結果:
從以上結果我們可知JSON.stringfy()
存在以下一些問題:
執行會報錯:存在BigInt
類型、循環引用。
拷貝Date
引用類型會變成字符串。
鍵值會消失:對象的值中為Function
、Undefined
、Symbol
這幾種類型,。
鍵值變成空對象:對象的值中為Map
、Set
、RegExp
這幾種類型。
無法拷貝:不可枚舉屬性、對象的原型鏈。
補充:其他更詳細的內容請查看官方文檔:JSON.stringify()
由于以上種種限制條件,JSON.stringfy()
方式僅限于深拷貝一些普通的對象,對于更復雜的數據類型,我們需要另尋他路。
手動遞歸實現深拷貝,我們只需要完成以下2點即可:
對于基礎類型,我們只需要簡單地賦值即可(使用=
)。
對于引用類型,我們需要創建新的對象,并通過遍歷鍵來賦值對應的值,這個過程中如果遇到 Object 類型還需要再次進行遍歷。
function deepClone(target) {
if (typeof target === 'object' && target) {
let cloneObj = {}
for (const key in target) { // 遍歷
const val = target[key]
if (typeof val === 'object' && val) {
cloneObj[key] = deepClone(val) // 是對象就再次調用該函數遞歸
} else {
cloneObj[key] = val // 基本類型的話直接復制值
}
}
return cloneObj
} else {
return target;
}
}
// 開頭的測試obj存在循環引用,除去這個條件進行測試
const clonedObj = deepClone(obj)
// 測試
clonedObj === obj // false,返回的是一個新對象
clonedObj.arr === obj.arr // false,說明拷貝的不是引用
瀏覽器執行結果:
該基礎版本存在許多問題:
不能處理循環引用。
只考慮了Object
對象,而Array
對象、Date
對象、RegExp
對象、Map
對象、Set
對象都變成了Object
對象,且值也不正確。
丟失了屬性名為Symbol
類型的屬性。
丟失了不可枚舉的屬性。
原型上的屬性也被添加到拷貝的對象中了。
如果存在循環引用的話,以上代碼會導致無限遞歸,從而使得堆棧溢出。如下例子:
const a = {}
const b = {}
a.b = b
b.a = a
deepClone(a)
對象 a 的鍵 b 指向對象 b,對象 b 的鍵 a 指向對象 a,查看a
對象,可以看到是無限循環的:
對對象a
執行深拷貝,會出現死循環,從而耗盡內存,進而報錯:堆棧溢出
如何避免這種情況呢?一種簡單的方式就是把已添加的對象記錄下來,這樣下次碰到相同的對象引用時,直接指向記錄中的對象即可。要實現這個記錄功能,我們可以借助 ES6 推出的 WeakMap
對象,該對象是一組鍵/值對的集合,其中的鍵是弱引用的。其鍵必須是對象,而值可以是任意的。(WeakMap
相關見這:WeakMap)
針對以上基礎版深拷貝存在的缺陷,我們進一步去完善,實現一個完美的深拷貝。
對于基礎版深拷貝存在的問題,我們一一改進:
存在的問題 | 改進方案 |
---|---|
1. 不能處理循環引用 | 使用 WeakMap 作為一個Hash表來進行查詢 |
2. 只考慮了Object 對象 | 當參數為 Date 、RegExp 、Function 、Map 、Set ,則直接生成一個新的實例返回 |
3. 屬性名為Symbol 的屬性4. 丟失了不可枚舉的屬性 | 針對能夠遍歷對象的不可枚舉屬性以及 Symbol 類型,我們可以使用 Reflect.ownKeys()注: Reflect.ownKeys(obj) 相當于[...Object.getOwnPropertyNames(obj), ...Object.getOwnPropertySymbols(obj)] |
4. 原型上的屬性 | Object.getOwnPropertyDescriptors()設置屬性描述對象,以及Object.create()方式繼承原型鏈 |
代碼實現:
function deepClone(target) {
// WeakMap作為記錄對象Hash表(用于防止循環引用)
const map = new WeakMap()
// 判斷是否為object類型的輔助函數,減少重復代碼
function isObject(target) {
return (typeof target === 'object' && target ) || typeof target === 'function'
}
function clone(data) {
// 基礎類型直接返回值
if (!isObject(data)) {
return data
}
// 日期或者正則對象則直接構造一個新的對象返回
if ([Date, RegExp].includes(data.constructor)) {
return new data.constructor(data)
}
// 處理函數對象
if (typeof data === 'function') {
return new Function('return ' + data.toString())()
}
// 如果該對象已存在,則直接返回該對象
const exist = map.get(data)
if (exist) {
return exist
}
// 處理Map對象
if (data instanceof Map) {
const result = new Map()
map.set(data, result)
data.forEach((val, key) => {
// 注意:map中的值為object的話也得深拷貝
if (isObject(val)) {
result.set(key, clone(val))
} else {
result.set(key, val)
}
})
return result
}
// 處理Set對象
if (data instanceof Set) {
const result = new Set()
map.set(data, result)
data.forEach(val => {
// 注意:set中的值為object的話也得深拷貝
if (isObject(val)) {
result.add(clone(val))
} else {
result.add(val)
}
})
return result
}
// 收集鍵名(考慮了以Symbol作為key以及不可枚舉的屬性)
const keys = Reflect.ownKeys(data)
// 利用 Object 的 getOwnPropertyDescriptors 方法可以獲得對象的所有屬性以及對應的屬性描述
const allDesc = Object.getOwnPropertyDescriptors(data)
// 結合 Object 的 create 方法創建一個新對象,并繼承傳入原對象的原型鏈, 這里得到的result是對data的淺拷貝
const result = Object.create(Object.getPrototypeOf(data), allDesc)
// 新對象加入到map中,進行記錄
map.set(data, result)
// Object.create()是淺拷貝,所以要判斷并遞歸執行深拷貝
keys.forEach(key => {
const val = data[key]
if (isObject(val)) {
// 屬性值為 對象類型 或 函數對象 的話也需要進行深拷貝
result[key] = clone(val)
} else {
result[key] = val
}
})
return result
}
return clone(target)
}
// 測試
const clonedObj = deepClone(obj)
clonedObj === obj // false,返回的是一個新對象
clonedObj.arr === obj.arr // false,說明拷貝的不是引用
clonedObj.func === obj.func // false,說明function也復制了一份
clonedObj.proto // proto,可以取到原型的屬性
在遍歷 Object
類型數據時,我們需要把 Symbol
類型的鍵名也考慮進來,所以不能通過 Object.keys
獲取鍵名或 for...in
方式遍歷,而是通過Reflect.ownKeys()
獲取所有自身的鍵名(getOwnPropertyNames
和 getOwnPropertySymbols
函數將鍵名組合成數組也行:[...Object.getOwnPropertyNames(obj), ...Object.getOwnPropertySymbols(obj)]
),然后再遍歷遞歸,最終實現拷貝。
瀏覽器執行結果:
可以發現我們的cloneObj對象
和原來的obj對象
一模一樣,并且修改cloneObj對象
的各個屬性都不會對obj對象
造成影響。其他的大家再多嘗試體會哦!
以上就是“JavaScript中的深拷貝如何實現”這篇文章的所有內容,感謝各位的閱讀!相信大家閱讀完這篇文章都有很大的收獲,小編每天都會為大家更新不同的知識,如果還想學習更多的知識,請關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。