JavaScript基礎(chǔ)心法 深淺拷貝(淺拷貝和深拷貝)
前言
說到深淺拷貝,必須先提到的是JavaScript的數(shù)據(jù)類型,之前的一篇文章JavaScript基礎(chǔ)心法——數(shù)據(jù)類型說的很清楚了,這里就不多說了。
需要知道的就是一點(diǎn):JavaScript的數(shù)據(jù)類型分為基本數(shù)據(jù)類型和引用數(shù)據(jù)類型。
對(duì)于基本數(shù)據(jù)類型的拷貝,并沒有深淺拷貝的區(qū)別,我們所說的深淺拷貝都是對(duì)于引用數(shù)據(jù)類型而言的。
淺拷貝
淺拷貝的意思就是只復(fù)制引用,而未復(fù)制真正的值。
const originArray = [1,2,3,4,5]; const originObj = {a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}}; const cloneArray = originArray; const cloneObj = originObj; console.log(cloneArray); // [1,2,3,4,5] console.log(originObj); // {a:'a',b:'b',c:Array[3],d:{dd:'dd'}} cloneArray.push(6); cloneObj.a = {aa:'aa'}; console.log(cloneArray); // [1,2,3,4,5,6] console.log(originArray); // [1,2,3,4,5,6] console.log(cloneObj); // {a:{aa:'aa'},b:'b',c:Array[3],d:{dd:'dd'}} console.log(originArray); // {a:{aa:'aa'},b:'b',c:Array[3],d:{dd:'dd'}}
上面的代碼是最簡(jiǎn)單的利用 = 賦值操作符實(shí)現(xiàn)了一個(gè)淺拷貝,可以很清楚的看到,隨著 cloneArray
和 cloneObj
改變,originArray
和 originObj
也隨著發(fā)生了變化。
深拷貝
深拷貝就是對(duì)目標(biāo)的完全拷貝,不像淺拷貝那樣只是復(fù)制了一層引用,就連值也都復(fù)制了。
只要進(jìn)行了深拷貝,它們老死不相往來,誰也不會(huì)影響誰。
目前實(shí)現(xiàn)深拷貝的方法不多,主要是兩種:
- 利用 JSON 對(duì)象中的 parse 和 stringify
- 利用遞歸來實(shí)現(xiàn)每一層都重新創(chuàng)建對(duì)象并賦值
JSON.stringify/parse的方法
先看看這兩個(gè)方法吧:
The JSON.stringify() method converts a JavaScript value to a JSON string.
JSON.stringify
是將一個(gè) JavaScript 值轉(zhuǎn)成一個(gè) JSON 字符串。
The JSON.parse() method parses a JSON string, constructing the JavaScript value or object described by the string.
JSON.parse
是將一個(gè) JSON 字符串轉(zhuǎn)成一個(gè) JavaScript 值或?qū)ο蟆?/p>
很好理解吧,就是 JavaScript 值和 JSON 字符串的相互轉(zhuǎn)換。
它能實(shí)現(xiàn)深拷貝呢?我們來試試。
const originArray = [1,2,3,4,5]; const cloneArray = JSON.parse(JSON.stringify(originArray)); console.log(cloneArray === originArray); // false const originObj = {a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}}; const cloneObj = JSON.parse(JSON.stringify(originObj)); console.log(cloneObj === originObj); // false cloneObj.a = 'aa'; cloneObj.c = [1,1,1]; cloneObj.d.dd = 'doubled'; console.log(cloneObj); // {a:'aa',b:'b',c:[1,1,1],d:{dd:'doubled'}}; console.log(originObj); // {a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}};
確實(shí)是深拷貝,也很方便。但是,這個(gè)方法只能適用于一些簡(jiǎn)單的情況。比如下面這樣的一個(gè)對(duì)象就不適用:
const originObj = { name:'axuebin', sayHello:function(){ console.log('Hello World'); } } console.log(originObj); // {name: "axuebin", sayHello: ƒ} const cloneObj = JSON.parse(JSON.stringify(originObj)); console.log(cloneObj); // {name: "axuebin"}
發(fā)現(xiàn)在 cloneObj
中,有屬性丟失了。。。那是為什么呢?
在 MDN 上找到了原因:
If undefined, a function, or a symbol is encountered during conversion it is either omitted (when it is found in an object) or censored to null (when it is found in an array). JSON.stringify can also just return undefined when passing in "pure" values like JSON.stringify(function(){}) or JSON.stringify(undefined).
undefined
、function
、symbol
會(huì)在轉(zhuǎn)換過程中被忽略。。。
明白了吧,就是說如果對(duì)象中含有一個(gè)函數(shù)時(shí)(很常見),就不能用這個(gè)方法進(jìn)行深拷貝。
遞歸的方法
遞歸的思想就很簡(jiǎn)單了,就是對(duì)每一層的數(shù)據(jù)都實(shí)現(xiàn)一次 創(chuàng)建對(duì)象->對(duì)象賦值 的操作,簡(jiǎn)單粗暴上代碼:
function deepClone(source){ const targetObj = source.constructor === Array ? [] : {}; // 判斷復(fù)制的目標(biāo)是數(shù)組還是對(duì)象 for(let keys in source){ // 遍歷目標(biāo) if(source.hasOwnProperty(keys)){ if(source[keys] && typeof source[keys] === 'object'){ // 如果值是對(duì)象,就遞歸一下 targetObj[keys] = source[keys].constructor === Array ? [] : {}; targetObj[keys] = deepClone(source[keys]); }else{ // 如果不是,就直接賦值 targetObj[keys] = source[keys]; } } } return targetObj; }
我們來試試:
const originObj = {a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}}; const cloneObj = deepClone(originObj); console.log(cloneObj === originObj); // false cloneObj.a = 'aa'; cloneObj.c = [1,1,1]; cloneObj.d.dd = 'doubled'; console.log(cloneObj); // {a:'aa',b:'b',c:[1,1,1],d:{dd:'doubled'}}; console.log(originObj); // {a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}};
可以。那再試試帶有函數(shù)的:
const originObj = { name:'axuebin', sayHello:function(){ console.log('Hello World'); } } console.log(originObj); // {name: "axuebin", sayHello: ƒ} const cloneObj = deepClone(originObj); console.log(cloneObj); // {name: "axuebin", sayHello: ƒ}
也可以。搞定。
是不是以為這樣就完了?? 當(dāng)然不是。
JavaScript中的拷貝方法
我們知道在 JavaScript 中,數(shù)組有兩個(gè)方法 concat 和 slice 是可以實(shí)現(xiàn)對(duì)原數(shù)組的拷貝的,這兩個(gè)方法都不會(huì)修改原數(shù)組,而是返回一個(gè)修改后的新數(shù)組。
同時(shí),ES6 中 引入了 Object.assgn
方法和 ... 展開運(yùn)算符也能實(shí)現(xiàn)對(duì)對(duì)象的拷貝。
那它們是淺拷貝還是深拷貝呢?
concat
The concat() method is used to merge two or more arrays. This method does not change the existing arrays, but instead returns a new array.
該方法可以連接兩個(gè)或者更多的數(shù)組,但是它不會(huì)修改已存在的數(shù)組,而是返回一個(gè)新數(shù)組。
看著這意思,很像是深拷貝啊,我們來試試:
const originArray = [1,2,3,4,5]; const cloneArray = originArray.concat(); console.log(cloneArray === originArray); // false cloneArray.push(6); // [1,2,3,4,5,6] console.log(originArray); [1,2,3,4,5];
看上去是深拷貝的。
我們來考慮一個(gè)問題,如果這個(gè)對(duì)象是多層的,會(huì)怎樣。
const originArray = [1,[1,2,3],{a:1}]; const cloneArray = originArray.concat(); console.log(cloneArray === originArray); // false cloneArray[1].push(4); cloneArray[2].a = 2; console.log(originArray); // [1,[1,2,3,4],{a:2}]
originArray
中含有數(shù)組 [1,2,3]
和對(duì)象 {a:1}
,如果我們直接修改數(shù)組和對(duì)象,不會(huì)影響 originArray
,但是我們修改數(shù)組 [1,2,3]
或?qū)ο?code> {a:1} 時(shí),發(fā)現(xiàn) originArray
也發(fā)生了變化。
結(jié)論:concat 只是對(duì)數(shù)組的第一層進(jìn)行深拷貝。
slice
The slice() method returns a shallow copy of a portion of an array into a new array object selected from begin to end (end not included). The original array will not be modified.
解釋中都直接寫道是 a shallow copy 了 ~
但是,并不是!
const originArray = [1,2,3,4,5]; const cloneArray = originArray.slice(); console.log(cloneArray === originArray); // false cloneArray.push(6); // [1,2,3,4,5,6] console.log(originArray); [1,2,3,4,5];
同樣地,我們?cè)囋嚩鄬拥臄?shù)組。
const originArray = [1,[1,2,3],{a:1}]; const cloneArray = originArray.slice(); console.log(cloneArray === originArray); // false cloneArray[1].push(4); cloneArray[2].a = 2; console.log(originArray); // [1,[1,2,3,4],{a:2}]
果然,結(jié)果和 concat 是一樣的。
結(jié)論:slice 只是對(duì)數(shù)組的第一層進(jìn)行深拷貝。
Object.assign()
The Object.assign() method is used to copy the values of all enumerable own properties from one or more source objects to a target object. It will return the target object.
復(fù)制復(fù)制復(fù)制。
那到底是淺拷貝還是深拷貝呢?
自己試試吧。。
結(jié)論:Object.assign() 拷貝的是屬性值。假如源對(duì)象的屬性值是一個(gè)指向?qū)ο蟮囊茫仓豢截惸莻€(gè)引用值。
... 展開運(yùn)算符
const originArray = [1,2,3,4,5,[6,7,8]]; const originObj = {a:1,b:{bb:1}}; const cloneArray = [...originArray]; cloneArray[0] = 0; cloneArray[5].push(9); console.log(originArray); // [1,2,3,4,5,[6,7,8,9]] const cloneObj = {...originObj}; cloneObj.a = 2; cloneObj.b.bb = 2; console.log(originObj); // {a:1,b:{bb:2}}
結(jié)論:... 實(shí)現(xiàn)的是對(duì)象第一層的深拷貝。后面的只是拷貝的引用值。
首層淺拷貝
我們知道了,會(huì)有一種情況,就是對(duì)目標(biāo)對(duì)象的第一層進(jìn)行深拷貝,然后后面的是淺拷貝,可以稱作“首層淺拷貝”。
我們可以自己實(shí)現(xiàn)一個(gè)這樣的函數(shù):
function shallowClone(source) { const targetObj = source.constructor === Array ? [] : {}; // 判斷復(fù)制的目標(biāo)是數(shù)組還是對(duì)象 for (let keys in source) { // 遍歷目標(biāo) if (source.hasOwnProperty(keys)) { targetObj[keys] = source[keys]; } } return targetObj; }
我們來測(cè)試一下:
const originObj = {a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}}; const cloneObj = shallowClone(originObj); console.log(cloneObj === originObj); // false cloneObj.a='aa'; cloneObj.c=[1,1,1]; cloneObj.d.dd='surprise';
經(jīng)過上面的修改,cloneObj
不用說,肯定是 {a:'aa',b:'b',c:[1,1,1],d:{dd:'surprise'}} 了,那 originObj 呢?剛剛我們驗(yàn)證了 cloneObj === originObj 是 false,說明這兩個(gè)對(duì)象引用地址不同啊,那應(yīng)該就是修改了 cloneObj 并不影響 originObj。
console.log(cloneObj); // {a:'aa',b:'b',c:[1,1,1],d:{dd:'surprise'}} console.log(originObj); // {a:'a',b:'b',c:[1,2,3],d:{dd:'surprise'}}
What happend?
originObj 中關(guān)于 a、c都沒被影響,但是 d 中的一個(gè)對(duì)象被修改了。。。說好的深拷貝呢?不是引用地址都不一樣了嗎?
原來是這樣:
- 從 shallowClone 的代碼中我們可以看出,我們只對(duì)第一層的目標(biāo)進(jìn)行了 深拷貝 ,而第二層開始的目標(biāo)我們是直接利用 = 賦值操作符進(jìn)行拷貝的。
- so,第二層后的目標(biāo)都只是復(fù)制了一個(gè)引用,也就是淺拷貝。
總結(jié)
- 賦值運(yùn)算符 = 實(shí)現(xiàn)的是淺拷貝,只拷貝對(duì)象的引用值;
- JavaScript 中數(shù)組和對(duì)象自帶的拷貝方法都是“首層淺拷貝”;
- JSON.stringify 實(shí)現(xiàn)的是深拷貝,但是對(duì)目標(biāo)對(duì)象有要求;
- 若想真正意義上的深拷貝,請(qǐng)遞歸。
相關(guān)文章
在JavaScript中處理時(shí)間之getHours()方法的使用
這篇文章主要介紹了在JavaScript中處理時(shí)間之getHours()方法的使用,是JS入門學(xué)些中的基礎(chǔ)知識(shí),需要的朋友可以參考下2015-06-06javascript數(shù)組里的27個(gè)方法總合詳解
這篇文章主要為大家詳細(xì)介紹了javascript數(shù)組里的方法27個(gè)總合,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下,希望能夠給你帶來幫助2022-02-02Javascript中eval函數(shù)的使用方法與示例
JavaScript有許多小竅門來使編程更加容易。其中之一就是eval()函數(shù),這個(gè)函數(shù)可以把一個(gè)字符串當(dāng)作一個(gè)JavaScript表達(dá)式一樣去執(zhí)行它。以下是它的說明2007-04-04JavaScript面向?qū)ο笾甤lass繼承類案例講解
這篇文章主要介紹了JavaScript面向?qū)ο笾甤lass繼承類案例講解,本篇文章通過簡(jiǎn)要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2021-08-08JavaScript實(shí)現(xiàn)表單元素的操作
這篇文章介紹了JavaScript操作表單元素的方法,文中通過示例代碼介紹的非常詳細(xì)。對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-05-05Javascript獲取窗口(容器)的大小及位置參數(shù)列舉及簡(jiǎn)要說明
Javascript獲取窗口(容器)的大小及位置一系列的東西比較多,容易混淆,在這里列舉及簡(jiǎn)要說明下,需要的朋友可以參考下2012-12-12onkeyup,onkeydown和onkeypress的區(qū)別介紹
三者在事件的響應(yīng)上還有一點(diǎn)不同,就是onkeydown 、onkeypress事件響應(yīng)的時(shí)候輸入的字符并沒有被系統(tǒng)接受,而響應(yīng)onkeyup的時(shí)候,輸入流已經(jīng)被系統(tǒng)接受2013-10-10