摘要:在以上討論和研究結(jié)束后,同學(xué)向我推薦了一個庫,測試了一下該庫存在方法,實(shí)現(xiàn)深拷貝更為完整和精致,前文問題均沒有在該方法內(nèi)被發(fā)現(xiàn),在這里提一波。
如果本文對您有任何幫助或者您有任何想要提出的意見或問題,請在本文下方回復(fù),誠摯歡迎各位參與討論,望各位不吝指教。
原載自己的小博客 JavaScript對象拷貝遇到的坑和解決方法 | 手柄君的小閣,所以無恥地算原創(chuàng)吧
近期參與某集訓(xùn),JavaScript,遇到一對象拷貝問題,得到需求:
給一個對象,請編寫一個函數(shù),使其可以拷貝一個對象,返回這個拷貝得到的新對象:
舉例如下:
function clone(obj){ //DO SOMETHING return newObject; //返回拷貝得到的新對象 }
首先想到解法如下:
> ES6解構(gòu)賦值(淺拷貝):function clone(obj){ return {...obj}; }
得到新對象為原始對象淺拷貝,即屬性Key一致,值如果是數(shù)或者字符串則值傳遞,否則為地址傳遞,即Value引用和源對象一致,可根據(jù)下方運(yùn)行測試:
var a = {a:1, b:2, c:3, d:[0, 1, 2]} var b = clone(a); console.log(b.d[1]); //1 b.d[1] = 2; console.log(b.d[1]); //2 console.log(a.d[1]); //2
對復(fù)制后的對象中包含的數(shù)組或者對象進(jìn)行編輯,影響了源對象,這顯然不是我們想要的結(jié)果,但是在對象內(nèi)不包含數(shù)組或?qū)ο髸r,該方法不失為一個快速創(chuàng)建對象拷貝的實(shí)用方法。
在ES6中,Object提供了一個 assign() 方法,也可以實(shí)現(xiàn)相同效果
function clone(obj){ return Object.assign({},obj); }
運(yùn)行效果和前一種方式基本一致,根據(jù)MDN描述,Object.assign() 方法用于將所有可枚舉屬性的值從一個或多個源對象復(fù)制到目標(biāo)對象,允許至少兩個參數(shù),第一個參數(shù)為拷貝的目標(biāo)對象,在方法執(zhí)行結(jié)束后會被返回,其余參數(shù)將作為拷貝來源。
前面兩種方法均為淺拷貝,那么對于對象內(nèi)包含對象或數(shù)組的對象,我們該怎樣拷貝呢?
我們的老師提供了一種方法如下,缺陷稍后再談
function clone(obj) { var newobj = obj.constructor === Array ? [] : {}; if (typeof obj !== "object") { return obj; } else { for (var i in obj) { newobj[i] = typeof obj[i] === "object" ? clone(obj[i]) : obj[i]; } } return newobj; }
同樣使用前文中的測試數(shù)據(jù):
var a = {a:1, b:2, c:3, d:[0, 1, 2]} var b = clone(a); console.log(b.d[1]); //1 b.d[1] = 2; console.log(b.d[1]); //2 console.log(a.d[1]); //1
可見該方法可以正確地對對象進(jìn)行深拷貝,并根據(jù)參數(shù)類型為數(shù)組或?qū)ο筮M(jìn)行進(jìn)行判斷并分別處理,但是該方法有一定缺陷:
1,在存在Symbol類型屬性key時,無法正確拷貝,可以嘗試以下測試數(shù)據(jù):
var sym = Symbol(); var a = {a:1, b:2, c:3, d:[0, 1, 2], [sym]:"symValue"} var b = clone(a); b.d[1] = 2; console.log(b.d[1]); //2 console.log(a.d[1]); //1 console.log(a[sym]); //"symValue" console.log(b[sym]); //undefined
可以發(fā)現(xiàn)拷貝得到的對象b,不存在Symbol類型對象為屬性名的屬性。
那么可以發(fā)現(xiàn),問題主要出在For...in遍歷屬性無法獲得Symbol類型Key導(dǎo)致,那么有什么方法可以遍歷到這些呢?
在ES6中Reflect包含的靜態(tài)方法ownKeys() 可以獲取到這些key,根據(jù)MDN描述,這個方法獲取到的返回值等同于Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target))。
那么使用ES6解構(gòu)賦值和Reflect.ownKeys() 組合使用,改寫上文函數(shù),得到:
> ES6解構(gòu)賦值 & Reflect.ownKeys() 遍歷并遞歸(深拷貝):function clone(obj) { var newobj = obj.constructor === Array ? [...obj] : {...obj}; if (typeof obj !== "object") { return obj; } else { Reflect.ownKeys(newobj).forEach(i => { newobj[i] = typeof obj[i] === "object" ? clone(obj[i]) : obj[i]; }); } return newobj; }
運(yùn)行相同的測試語句:
var sym = Symbol(); var a = {a:1, b:2, c:3, d:[0, 1, 2], [sym]:"symValue"} var b = clone(a); b.d[1] = 2; console.log(b.d[1]); //2 console.log(a.d[1]); //1 console.log(a[sym]); //"symValue" console.log(b[sym]); //"symValue" b[sym] = "newValue"; console.log(a[sym]); //"symValue" console.log(b[sym]); //"newValue"
可以發(fā)現(xiàn)Symbol類型的key也被正確拷貝并賦值了,但是該方法依然有一定問題,如下:
2,在對象內(nèi)部存在環(huán)時,堆棧溢出,嘗試運(yùn)行以下測試語句:
var a = { info: "a", arr: [0, 1, 2] }; var b = { data: a, info: "b", arr: [3, 4, 5] }; a.data = b; var c = clone(a); //Error: Maximum call stack size exceeded. 報錯:堆棧溢出
解決這個的方法稍后再講,但目前來看已有的兩種深拷貝方法足夠平時使用,接下來正好提一下,ES5.1中包含的JSON對象,使用該對象亦可對對象進(jìn)行深拷貝,會遇到的問題和第一種深拷貝方式一樣,無法記錄Symbol為屬性名的屬性,另外只能包含能用JSON字符串表示的數(shù)據(jù)類型,實(shí)現(xiàn)代碼如下:
> JSON對象轉(zhuǎn)義(深拷貝):function clone(obj) { return JSON.parse(JSON.stringify(obj); }
JSON.stringify() 首先將對象序列化為字符串,再由JSON.parse() 反序列化為對象,形成新的對象。
回到前面提到的問題2,如果對象內(nèi)包含環(huán),怎么辦,我的實(shí)現(xiàn)思路為使用兩個對象作為類似HashMap,記錄源對象的結(jié)構(gòu),并在每層遍歷前檢查對象是否已經(jīng)被拷貝過,如果是則重新指向到拷貝好的對象,防止無限遞歸。實(shí)現(xiàn)代碼如下(配有注釋):
:
/** * 深拷貝(包括Symbol) * @param {Object} obj */ function clone(obj) { const map = {}; //空對象,記錄源對象 const mapCopy = {}; //空對象,記錄拷貝對象 /** * 在theThis對象中,查找e對象的key,如果找不到,返回false * @param {Object} e 要查找的對象 * @param {Object} theThis 在該對象內(nèi)查找 * @returns {symbol | boolean} */ function indexOfFun(e, theThis) { let re = false; for (const key of Reflect.ownKeys(theThis)) { if (e === theThis[key]) { re = key; break; } } return re; } /** * 在Map對象中,查找e對象的key * @param {Object} e */ const indexOfMap = e => indexOfFun(e, map); /** * 在Map中記錄obj對象內(nèi)所有對象的地址 * @param {Object} obj 要被記錄的對象 */ function bindMap(obj) { map[Symbol()] = obj; Reflect.ownKeys(obj).forEach(key => { //當(dāng)屬性類型為Object且還沒被記錄過 if (typeof obj[key] === "object" && !indexOfMap(obj[key])) { bindMap(obj[key]); //記錄這個對象 } }); } bindMap(obj); /** * 拷貝對象 * @param {Object} obj 要被拷貝的對象 */ function copyObj(obj) { let re;//用作返回 if (Array.isArray(obj)) { re = [...obj]; //當(dāng)obj為數(shù)組 } else { re = { ...obj }; //當(dāng)obj為對象 } mapCopy[indexOfMap(obj)] = re; //記錄新對象的地址 Reflect.ownKeys(re).forEach(key => { //遍歷新對象屬性 if (typeof re[key] === "object") { //當(dāng)屬性類型為Object if (mapCopy[indexOfMap(re[key])]) { //當(dāng)屬性已經(jīng)被拷貝過 re[key] = mapCopy[indexOfMap(re[key])]; //修改屬性指向到先前拷貝好的對象 } else {//當(dāng)屬性還沒有被拷貝 re[key] = copyObj(re[key]); //拷貝這個對象,并將屬性指向新對象 } } }); return re; //返回拷貝的新對象 } return copyObj(obj); //執(zhí)行拷貝并返回 }
運(yùn)行前面的測試語句:
var a = { info: "a", arr: [0, 1, 2] }; var b = { data: a, info: "b", arr: [3, 4, 5] }; a.data = b; var c = clone(a); c.info = "c"; c.data.info = "d"; console.log(a.info); //"a" console.log(a.data.info); //"b" console.log(c.info); //"c" console.log(c.data.info); //"d"
得到該函數(shù)可以正確地拷貝帶環(huán)對象。
在以上討論和研究結(jié)束后,同學(xué)向我推薦了一個庫 lodash,測試了一下該庫存在 _.cloneDeep() 方法,實(shí)現(xiàn)深拷貝更為完整和精致,前文問題均沒有在該方法內(nèi)被發(fā)現(xiàn),在這里提一波。
如果本文對您有任何幫助或者您有任何想要提出的意見或問題,請在本文下方回復(fù),誠摯歡迎各位參與討論,望各位不吝指教。
本文原載于https://www.bysb.net/3113.html
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/107470.html
摘要:引用數(shù)據(jù)類型是存放在堆內(nèi)存中的,變量實(shí)際上是一個存放在棧內(nèi)存的指針,這個指針指向堆內(nèi)存中的地址。棧和堆的區(qū)別其實(shí)淺拷貝和深拷貝的主要區(qū)別就是數(shù)據(jù)在內(nèi)存中的存儲類型不同。這里,對存在子對象的對象進(jìn)行拷貝的時候,就是深拷貝了。 數(shù)據(jù)類型 在開始拷貝之前,我們從JavaScript的數(shù)據(jù)類型和內(nèi)存存放地址講起。數(shù)據(jù)類型分為基本數(shù)據(jù)類型 和引用數(shù)據(jù)類型 基本數(shù)據(jù)類型主要包括undefin...
摘要:相信人很多學(xué)習(xí)的過程中都踩了深拷貝和淺拷貝的坑,深拷貝和淺拷貝的區(qū)別我就不再贅述了,今天我來寫一下我自己實(shí)現(xiàn)深拷貝的各種方法。中的深拷貝也是用類似方法實(shí)現(xiàn)。 相信人很多學(xué)習(xí)js的過程中都踩了深拷貝和淺拷貝的坑,深拷貝和淺拷貝的區(qū)別我就不再贅述了,今天我來寫一下我自己實(shí)現(xiàn)深拷貝的各種方法。 比較簡單的拷貝方式可以借用瀏覽器的Json對象去實(shí)現(xiàn),先把對象轉(zhuǎn)化為json字符串,在解析回對...
摘要:兩者享有相同的引用。深拷貝這個問題通常可以通過來解決。深淺拷貝也可以使用的方法,注意使用合并返回值 前言 最近寫代碼經(jīng)常用到深淺拷貝,從一開始的悶頭使用漸漸想要深究其理,這篇文章記錄一下我的認(rèn)為,有所不足,恭請指正 我們可以先看看一個常遇到的一個小問題 let a = { age:1 } let b = a a.age = 2 console.log(b.age) //2 ...
摘要:深拷貝與淺拷貝的出現(xiàn),就與這兩個數(shù)據(jù)類型有關(guān)。這時,就需要用淺拷貝來實(shí)現(xiàn)了。數(shù)據(jù)一但過多,就會有遞歸爆棧的風(fēng)險。這個方法是在解決遞歸爆棧問題的基礎(chǔ)上,加以改進(jìn)解決循環(huán)引用的問題。但如果你并不想保持引用,那就改用用于解決遞歸爆棧即可。 前言 這是前端面試題系列的第 9 篇,你可能錯過了前面的篇章,可以在這里找到: 數(shù)組去重(10 種濃縮版) JavaScript 中的事件機(jī)制(從原生到...
摘要:所以,深拷貝是對對象以及對象的所有子對象進(jìn)行拷貝實(shí)現(xiàn)方式就是遞歸調(diào)用淺拷貝對于深拷貝的對象,改變源對象不會對得到的對象有影響。 為什么會有淺拷貝與深拷貝什么是淺拷貝與深拷貝如何實(shí)現(xiàn)淺拷貝與深拷貝好了,問題出來了,那么下面就讓我們帶著這幾個問題去探究一下吧! 如果文章中有出現(xiàn)紕漏、錯誤之處,還請看到的小伙伴多多指教,先行謝過 以下↓ 數(shù)據(jù)類型在開始了解 淺拷貝 與 深拷貝 之前,讓我們先...
閱讀 854·2023-04-26 00:11
閱讀 2655·2021-11-04 16:13
閱讀 2101·2021-09-09 09:33
閱讀 1472·2021-08-20 09:35
閱讀 3817·2021-08-09 13:42
閱讀 3604·2019-08-30 15:55
閱讀 1039·2019-08-30 15:55
閱讀 2218·2019-08-30 13:55