代碼實現深拷貝
經過對淺拷貝的簡單瞭解,咱們就到了如何實現深拷貝的問題上,常見的循環遍歷,只是遍歷了一層數據,明顯解決不了這個問題,不過咱們可以看出,深拷貝的問題不就在於怎麼解決無限層級拷貝問題嗎,這種數據類型似乎在哪見過,對沒錯,可以用遞歸解決!
咱們先對淺拷貝代碼進行改造,用了遞歸之後就實現了一個深拷貝。
const deepClone = (source) => {
const target = {};
for (const i in source) {
if (source.hasOwnProperty(i)
&& target[i] === 'object') {
target[i] = deepClone(source[i]); // 注意這裏
} else {
target[i] = source[i];
}
}
return target;
};
但是這份代碼還有一些細節需要修改,如:
-
沒有對參數進行校驗,如果傳入進來的不是對象或者數組,我們直接返回即可。
-
通過 typeof 判斷是否對象的邏輯不夠嚴謹。 typeof null
對此可進行改進,實現一份完整的深拷貝代碼。
1. 手寫深拷貝
// 定義檢測數據類型的功能函數
const checkedType = (target) => {
// console.log(Object.prototype.toString.call(target))
console.log(Object.prototype.toString.call(target));
return Object.prototype.toString.call(target).slice(8, -1);
}
// 實現深度克隆對象或者數組
const deepClone = (target) => {
// 判斷拷貝的數據類型
// 初始化變量 result 成爲最終數據
let result, targetType = checkedType(target);
if (targetType === 'Object') {
result = {};
} else if (targetType === 'Array') {
result = [];
} else {
return target;
}
// 遍歷目標數據
for (let i in target) {
// 獲取遍歷數據結構的每一項值
let value = target[i];
// 判斷目標結構裏的每一項值是否存在對象或者數組
if (checkedType(value) === 'Object' || checkedType(value) === 'Array') {
// 如果對象或者數組中還嵌套了對象或者數組,那麼繼續遍歷
result[i] = deepClone(value);
} else {
result[i] = value;
}
}
// 返回最終值
return result;
}
const obj1 = [
1,
'Hello!',
{ name: '張' },
[
{
name: '李',
}
],
]
const obj2 = deepClone(obj1);
obj2[0] = 2;
obj2[1] = 'Hi!';
obj2[2].name = '小張';
obj2[3][0].name = '小李';
console.log(obj1);
// [ 1, 'Hello!', { name: '張' }, [ { name: '李' } ] ]
console.log(obj2);
// [ 2, 'Hi!', { name: '小張' }, [ { name: '小李' } ] ]
經過深拷貝後,我們可以看到,再修改引用數據類型裏的值時,原對象毫不收到影響,說明這份代碼還是挺成功的。下面是代碼的實現思路:
-
Object.prototype.toString.call()
:穩健地判斷JavaScript
數據類型方式,可以符合預期的判斷基本數據類型String
、Undefined
等,也可以判斷Array
、Object
這些引用數據類型。 -
然後,我們通過方法
targetType()
中的Object.prototype.toString.call()
,判斷傳入的數據類型屬於那種,從而改變result
的值爲 {}、[] 或者直接返回傳入的值(return target)
。 -
最後,我們再通過
for...in
判斷target
的所有元素,如果屬於 {} 或者 [],那麼就遞歸再進行clone()
操作; -
如果是基本數據類型,則直接傳遞到數組中……從而在最後返回一個深拷貝的數據。
但是如果經過嚴格的測試,這份代碼也是行不通的,如果拷貝對象是個循環空對象,那麼咱們將把自己繞進去,寫了個死循環。而對廣度和深度進行測試後,數據大的話,也是行不通的,十分容易爆棧。
這就是手寫深拷貝待解決的兩個大問題:
- 死循環
- 爆棧
解決方法,博主也是瞭解的比較少,能簡單理解到:
- 死循環的解決是用
哈希表
進行循環檢測,我們設置一個數組或者哈希表存儲已拷貝過的對象,當檢測到當前對象已存在於哈希表中時,取出該值並返回即可。 - 爆棧的問題可用棧解決
這裏就不過多解釋了,有興趣的可去大佬文章深挖。
2. JSON. parse (JSON. stringify())
JSON. stringify()
:將對象轉成JSON 字符串。JSON. parse()
:將字符串解析成對象。
const arr1 = [
1,
{
username: 'zhangsan',
},
];
let arr2 = JSON.parse(JSON.stringify(arr1));
arr2[0] = 2;
arr2[1].username = 'li';
console.log(arr1);
// [ 1, { username: 'zhangsan' } ]
console.log(arr2);
// [ 2, { username: 'li' } ]
通過JSON. parse (JSON. stringify())
將JavaScript
對象轉序列化(轉換成JSON
字符串),再將其還原成JavaScript對象,一去一 來我們就產生了一個新的對象,而且對象會開闢新的棧,從而實現深拷貝。
此方法雖然簡單,但是卻有很多侷限性。
- 1、不能存放
函數
或者Undefined
,否則會丟失函數
或者Undefined
; - 2、不要存放時間對象,否則會變成字符串形式;
- 3、不能存放
RegExp
、Error
對象,否則會變成空對象; - 4、不能存放
NaN
、Infinity
、-Infinity
,否則會變成null
;
3.函數庫Lodash
Lodash
作爲一個JavaScript
函數庫/工具庫,它裏面有非常好用的封裝好的功能,大家可以去試試,這裏我們查看下它的cloneDeep()
方法,該方法會遞歸拷貝value
。
首先需要npm
先下載lodash
包
var lodash = require('lodash');
const obj1 = [
1,
'Hello!',
{ name: 'html' },
[
{
name: 'css',
}
],
]
const obj2 = lodash.cloneDeep(obj1);
obj2[0] = 2;
obj2[1] = 'Hi!';
obj2[2].name = 'lodash';
obj2[3][0].name = 'js';
console.log(obj1);
// [ 1, 'Hello!', { name: 'html' }, [ { name: 'css' } ] ]
console.log(obj2);
// [ 2, 'Hi!', { name: 'lodash' }, [ { name: 'js' } ] ]
4.框架jQuery中extend() 方法
const obj1 = [
1,
'Hello!',
{ name: 'html' },
[
{
name: 'css',
}
],
]
const obj2 = {};
/**
* @name jQuery深拷貝
* @description $.extend(deep, target, object1, object2...)
* @param {Boolean} deep 可選 true 或者 false,默認是 false,所以一般如果需要填寫,最好是 true。
* @param {Object} target 需要存放的位置
* @param {Object} object 可以有 n 個原數據
*/
$.extend(true, obj2, obj1);
obj2[0] = 2;
obj2[1] = 'Hi!';
obj2[2].name = 'js';
obj2[3][0].name = 'jq';
console.log(obj1);
console.log(obj2);
總結
之前瀏覽博客的時候,總是看到別人關於深拷貝與淺拷貝的文章,也知道面試這是常問的知識點,但是一直沒怎麼看。而在前不久,在小組輪到我講課了,思來想去講寫什麼,最後決定講這個,經過一星期的學習,真的學習到了不少,碰到任何不懂的問題,自己立馬去搜了搜,也連聯想了許多相關的知識點,也算是把這一難點學會了,雖然平時可能用不用到,但用不用的到並不是關鍵,要的是學會知識😁,一個深拷貝卻蘊含了那麼多的知識點,誰能想的到,平時可能也不會怎麼注意吧。
完整文章見: