工做中常常會遇到須要複製 JavaScript 數據的時候,遇到 bug 時實在使人頭疼;面試中也常常會被問到如何實現一個數據的深淺拷貝,可是你對其中的原理清晰嗎?一塊兒來看一下吧!
想要更加透徹的理解爲何 JavaScript 會有深淺拷貝,須要先了解下 JavaScript 的數據類型有哪些,通常分爲基本類型(Number、String、Null、Undefined、Boolean、Symbol )和引用類型(對象、數組、函數)。jquery
基本類型是不可變的,任何方法都沒法改變一個基本類型的值,也不能夠給基本類型添加屬性或者方法。可是能夠爲引用類型添加屬性和方法,也能夠刪除其屬性和方法。面試
基本類型和引用類型在內存中的存儲方式也大不相同,基本類型保存在棧內存中,而引用類型保存在堆內存中。爲何要分兩種保存方式呢? 由於保存在棧內存的必須是大小固定的數據,引用類型的大小不固定,只能保存在堆內存中,可是咱們能夠把它的地址寫在棧內存中以供咱們訪問。json
說來這麼多,咱們來看個示例:api
let num1 = 10; let obj1 = { name: "hh" } let num2 = num1; let obj2 = obj1; num2 = 20; obj2.name = "kk"; console.log(num1); // 10 console.log(obj1.name); // kk
執行完這段代碼,內存空間裏是這樣的:數組
能夠看到 obj1 和 obj2 都保存了一個指向該對象的指針,全部的操做都是對該引用的操做,因此對 obj2 的修改會影響 obj1。數據結構
小結:函數
之因此會出現深淺拷貝,是因爲 JS 對 基本類型和 引用類型的處理不一樣。 基本類型指的是簡單的數據段,而 引用類型指的是一個對象保存在堆內存中的地址,JS 不容許咱們直接操做內存中的地址,也就是說不能操做對象的內存空間,因此,咱們對對象的操做都只是在操做它的引用而已。在複製時也是同樣,若是咱們複製一個基本類型的值時,會建立一個新值,並把它保存在新的變量的位置上。而若是咱們複製一個引用類型時,一樣會把變量中的值複製一份放到新的變量空間裏,但此時複製的東西並非對象自己,而是指向該對象的指針。因此咱們複製引用類型後,兩個變量其實指向同一個對象,因此改變其中的一個對象,會影響到另一個。spa
淺拷貝只是複製基本類型的數據或者指向某個對象的指針,而不是複製對象自己,源對象和目標對象共享同一塊內存;若對目標對象進行修改,存在源對象被篡改的可能。指針
咱們來看下淺拷貝的實現:code
/* sourceObj 表示源對象 * 執行完函數,返回目標對象 */ function shadowClone (sourceObj = {}) { let targetObj = Array.isArray(sourceObj) ? [] : {}; let copy; for (var key in sourceObj) { copy = sourceObj[key]; targetObj[key] = copy; } return targetObj; }
// 定義 source let sourceObj = { number: 1, string: 'source1', boolean: true, null: null, undefined: undefined, arr: [{name: 'arr1'}, 1], func: () => 'sourceFunc1', obj: { string: 'obj1', func: () => 'objFunc1' } } // 拷貝sourceObj let copyObj = shadowClone(sourceObj); // 修改 sourceObj copyObj.number = 2; copyObj.string = 'source2'; copyObj.boolean = false; copyObj.arr[0].name = 'arr2'; copyObj.func = () => 'sourceFunc2'; copyObj.obj.string = 'obj2'; copyObj.obj.func = () => 'objFunc2'; // 執行 console.log(sourceObj); /* { number: 1, string: 'source1', boolean: true, null: null, undefined: undefined, arr: [{name: 'arr2'}], func: () => 'sourceFunc1', obj: { func: () => 'objFunc2', string: 'obj2' } } */
深拷貝可以實現真正意義上的對象的拷貝,實現方法就是遞歸調用「淺拷貝」。深拷貝會創造一個如出一轍的對象,其內容地址是自助分配的,拷貝結束以後,內存中的值是徹底相同的,可是內存地址是不同的,目標對象跟源對象不共享內存,修改任何一方的值,不會對另一方形成影響。
/* sourceObj 表示源對象 * 執行完函數,返回目標對象 */ function deepClone (sourceObj = {}) { let targetObj = Array.isArray(sourceObj) ? [] : {}; let copy; for (var key in sourceObj) { copy = sourceObj[key]; if (typeof(copy) === 'object') { if (copy instanceof Object) { targetObj[key] = deepClone(copy); } else { targetObj[key] = copy; } } else if (typeof(copy) === 'function') { targetObj[key] = eval(copy.toString()); } else { targetObj[key] = copy; } } return targetObj; }
// 定義 sourceObj let sourceObj = { number: 1, string: 'source1', boolean: true, null: null, undefined: undefined, arr: [{name: 'arr1'}], func: () => 'sourceFunc1', obj: { string: 'obj1', func: () => 'objFunc1' } } // 拷貝sourceObj let copyObj = deepClone(sourceObj); // 修改 source copyObj.number = 2; copyObj.string = 'source2'; copyObj.boolean = false; copyObj.arr[0].name = 'arr2'; copyObj.func = () => 'sourceFunc2'; copyObj.obj.string = 'obj2'; copyObj.obj.func = () => 'objFunc2'; // 執行 console.log(sourceObj); /* { number: 1, string: 'source1', boolean: true, null: null, undefined: undefined, arr: [{name: 'arr1'}], func: () => 'sourceFunc1', obj: { func: () => 'objFunc1', string: 'obj1' } } */
兩個方法能夠合併在一塊兒:
/* deep 爲 true 表示深複製,爲 false 表示淺複製 * sourceObj 表示源對象 * 執行完函數,返回目標對象 */ function clone (deep = true, sourceObj = {}) { let targetObj = Array.isArray(sourceObj) ? [] : {}; let copy; for (var key in sourceObj) { copy = sourceObj[key]; if (deep && typeof(copy) === 'object') { if (copy instanceof Object) { targetObj[key] = clone(deep, copy); } else { targetObj[key] = copy; } } else if (deep && typeof(copy) === 'function') { targetObj[key] = eval(copy.toString()); } else { targetObj[key] = copy; } } return targetObj; }
(1)若拷貝數組是純數據(不含對象),能夠經過concat() 和 slice() 來實現深拷貝;
let a = [1, 2]; let b = [3, 4]; let copy = a.concat(b); a[1] = 5; b[1] = 6; console.log(copy); // [1, 2, 3, 4]
let a = [1, 2]; let copy = a.slice(); copy[0] = 3; console.log(a); // [1, 2]
(2)若拷貝數組中有對象,可使用 concat() 和 slice() 方法來實現數組的淺拷貝。
let a = [1, {name: 'hh1'}]; let b = [2, {name: 'kk1'}]; let copy = a.concat(b); copy[1].name = 'hh2'; copy[3].name = 'kk2'; console.log(copy); // [1, {name: 'hh2'}, 2, {name: 'kk2'}]
不管 a[1].name 或者 b[1].name 改變,copy[1].name 的值都會改變。
let a = [1, {name: 'hh1'}]; let copy = a.slice(); copy[1].name = 'hh2'; console.log(a); // [1, {name: 'hh2'}]
改變了 a[1].name 後,copy[1].name 的值也改變了。
Object.assign()、Object.create() 都是一層(根級)深拷貝,之下的級別爲淺拷貝。
(1) 若拷貝對象只有一級,能夠經過 Object.assign()、Object.create() 來實現對象的深拷貝;
let sourceObj = { str: 'hh1', number: 10 } let targetObj = Object.assign({}, sourceObj) targetObj.str = 'hh2' console.log(sourceObj); // {str: 'hh1', number: 10}
let sourceObj = { str: 'hh1', number: 10 } let targetObj = Object.create(sourceObj) targetObj.str = 'hh2' console.log(sourceObj); // {str: 'hh1', number: 10}
(2) 若拷貝對象有多級, Object.assign()、Object.create() 實現的是對象的淺拷貝。
let sourceObj = { str: 'hh', number: 10, obj: { str: 'kk1' } } let targetObj = Object.assign({}, sourceObj) targetObj.obj.str = 'kk2' console.log(sourceObj); // { // str: 'hh', // number: 10, // obj: { // str: 'kk2' // } // }
let sourceObj = { str: 'hh', number: 10, obj: { str: 'kk1' } } let targetObj = Object.create(sourceObj) targetObj.obj.str = 'kk2' console.log(sourceObj); // { // str: 'hh', // number: 10, // obj: { // str: 'kk2' // } // }
修改了 targetObj.obj.str 的值以後,sourceObj.obj.str 的值也改變了。
對象的解構同 Object.assign() 和 Object.create(),都是一層(根級)深拷貝,之下的級別爲淺拷貝。
(1)若拷貝對象只有一層,能夠經過對象的解構來實現深拷貝;
let sourceObj = { str: 'hh1', number: 10 } let targetObj = {...sourceObj}; targetObj.str = 'hh2' console.log(sourceObj); // {str: 'hh1', number: 10}
(2)若拷貝對象有多層,經過對象的解構實現的是對象的淺拷貝。
let sourceObj = { str: 'hh', number: 10, obj: { str: 'kk1' } } let targetObj = {...sourceObj}; targetObj.obj.str = 'kk2' console.log(sourceObj); // { // str: 'hh', // number: 10, // obj: { // str: 'kk2' // } // }
用 JSON.stringify() 把對象轉成字符串,再用 JSON.parse() 把字符串轉成新的對象,能夠實現對象的深複製。
let source = ['hh', 1, [2, 3], {name: 'kk1'}]; let copy = JSON.parse(JSON.stringify(source)); copy[2][1] = 4; copy[3].name = 'kk2'; console.log(source); // ['hh', 1, [2, 3], {name: 'kk1'}]
能夠看出,雖然改變了 copy[2].name 的值,可是 source[2].name 的值沒有改變。
JSON.parse(JSON.stringify(obj)) 不只能複製數組還能夠複製對象,可是幾個弊端:
1)它會拋棄對象的 constructor,深拷貝以後,無論這個對象原來的構造函數是什麼,在深拷貝以後都會變成 Object;
2)這種方法能正確處理的對象只有 Number, String, Boolean, Array, 扁平對象,即那些可以被 json 直接表示的數據結構。RegExp 對象是沒法經過這種方式深拷貝。
3)只有能夠轉成 JSON 格式的對象才能夠這樣用,像 function 沒辦法轉成 JSON。
如下兩種庫都能實現深淺拷貝,有各自的使用方法。
具體使用能夠參考:官方文檔
具體使用能夠參考:官方文檔